<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick, inject } from 'vue'
import { configService } from '../services/configService'
import { attackDataService, type AttackChainData } from '../services/attackDataService'
import { csvService, type TripleStructure } from '../services/csvService'

// 注入导航方法
const navigateToAttackEffect = inject<(ip: string) => void>('navigateToAttackEffect', () => {})

// 注意：SSE连接已改为页面级别管理，不再在TopologyDesigner中清除QPS数据
// 清除操作将在AttackEffectMonitor页面中自动处理

type BackendConfig = {
  ip: string
  query_A_times: number
  query_AAAA_times: number
  retry_total_time_window: number | null
  max_fanout_num: number | null
  backend_query_merge_time_window: number | null
  request_auth_times?: number
  auth_time_index?: number
  request_vic_times?: number
  vic_time_index?: number
  cluster_group?: number  // 聚类组编号，从0开始
}

type AvailableNode = { 
  id: number
  name: string
  backends: number
  frontendIp?: string
  backendIps?: string[]
  level?: number
  isDeployed?: boolean
  // 服务器节点的额外参数
  rd_senstive_flage?: boolean
  negative_cache?: number | null
  frontend_query_merge_time_window?: number | null
  backendConfigs?: BackendConfig[]
  // 新增的统计字段
  overall_auth_duration?: number
  overall_vic_duration?: number
  total_request_auth_times?: number
  total_request_vic_times?: number
}

type BackendGroup = {
  backendIndices: number[]  // 这组后端包含的索引
  targetNodeId?: number | 'target'  // 连接到的目标节点ID
  expanded?: boolean  // 这个组是否展开
}

type CanvasNode = { 
  id: number
  name: string
  backends: number
  x: number
  y: number
  frontendIp?: string
  backendIps?: string[]
  sourceAvailableNodeId?: number
  level?: number
  backendExpanded?: boolean  // 是否展开后端（默认false）
  backendGroups?: BackendGroup[]  // 后端分组信息（自动编排时使用）
  isAutoArranged?: boolean  // 是否由自动编排生成
  // 服务器节点的额外参数
  rd_senstive_flage?: boolean
  negative_cache?: number | null
  frontend_query_merge_time_window?: number | null
  backendConfigs?: BackendConfig[]
  // 新增的统计字段
  overall_auth_duration?: number
  overall_vic_duration?: number
  total_request_auth_times?: number
  total_request_vic_times?: number
  // 态势感知状态字段
  frontendStatus?: number  // 前端状态：0=未调用(红色), 1=已调用(绿色)
  backendStatus?: Record<string, number>  // 后端状态：IP -> status (使用普通对象而不是Map以确保响应式)
}
type Port = { nodeId: number | 'target'; kind: 'frontend' | 'backend' | 'target'; backendIndex?: number }
type Connection = { from: Port; to: Port }

// 可用节点列表，通过右侧表单新增
const availableNodes = ref<AvailableNode[]>([])

// 配置相关状态
const configLoaded = ref(false)
const targetServerInfo = ref('')
const sendingStatus = ref<'idle' | 'sending' | 'success' | 'error'>('idle')
const lastSendResult = ref('')



// 态势感知相关状态
const isMonitoring = ref(false)  // 是否正在监控
const monitoringTimer = ref<number | null>(null)  // 定时器ID
const attackTaskId = ref<string>('')  // 攻击任务ID，从开始攻击的响应中获取

// 攻击配置对话框状态
const showAttackConfigDialog = ref(false)
const attackDomainRange = ref('a.attack{i}.ns4.48232025.xyz')  // 攻击域名范围，默认值（实网模式）
const attackDomainIncrement = ref(1)  // 自增字段的值，默认1
const attackInterval = ref(0)  // 每轮次攻击时间间隔（秒），默认0
const attackRounds = ref(1)  // 总攻击轮次数量，默认1

// 测试床/实网切换状态
const environment = ref<'testbed' | 'realnet'>('testbed')  // 默认测试床

// 自激选项（仅在测试床模式下使用）
const selfExcitation = ref(false)  // 默认关闭

// 弹出窗口状态
const showModal = ref(false)
const modalNodeName = ref('')
const modalNodeBackends = ref(2)
const modalNodeLevel = ref(1)
const modalFrontendIp = ref('')
const modalBackendIps = ref<string[]>([])
const editingNodeId = ref<number | null>(null) // 正在编辑的节点ID

// 服务器节点的额外参数
const modalRdSenstiveFlage = ref<boolean | undefined>(undefined)
const modalNegativeCache = ref<number | null>(null)
const modalFrontendQueryMergeTimeWindow = ref<number | null>(null)
const modalBackendConfigs = ref<BackendConfig[]>([])

// 保存编辑时的原始数据备份（用于取消时恢复）
const originalNodeData = ref<{
  name: string
  level: number
  frontendIp: string
  backendIps: string[]
  backends: number
  backendConfigs: BackendConfig[]
  rd_senstive_flage?: boolean
  negative_cache?: number | null
  frontend_query_merge_time_window?: number | null
  overall_auth_duration?: number
  overall_vic_duration?: number
  total_request_auth_times?: number
  total_request_vic_times?: number
} | null>(null)

// 前端统计字段
const modalOverallAuthDuration = ref<number | undefined>(undefined)
const modalOverallVicDuration = ref<number | undefined>(undefined)
const modalTotalRequestAuthTimes = ref<number | undefined>(undefined)
const modalTotalRequestVicTimes = ref<number | undefined>(undefined)

// 高级参数展开状态
const showAdvancedParams = ref(false)
const showBackendConfigs = ref(false)

// 任务选择窗口状态
const showTaskSelectionModal = ref(false)
const taskList = ref<Array<{ id: number; task_id: string; start_time: string; end_time: string | null; status: number; node_count?: number }>>([])
const selectedTaskIds = ref<string[]>([])
const loadingTasks = ref(false)
const loadingNodes = ref(false)

// 节点排序相关状态
const showSortMenu = ref(false)
const sortingStatus = ref<'idle' | 'sorting' | 'success' | 'error'>('idle')
const sortMessage = ref('')
const sortButtonWrapper = ref<HTMLElement | null>(null)

// 配置选项窗口状态
const showConfigModal = ref(false)
const configDepth = ref<number | null>(null)
const configFanout = ref<number | null>(null)
const chainCount = ref<number | null>(1) // 攻击链条数，默认为1
const configStatus = ref<'idle' | 'saving' | 'success' | 'error'>('idle')
const configMessage = ref('')
const isConfigAndArrange = ref(false) // 标记是否在配置后执行编排

const clusterCount = ref<number | null>(null)
const clusterStatus = ref<'idle' | 'saving' | 'success' | 'error'>('idle')
const clusterMessage = ref('')

// 攻击链优化相关状态
const optimizeEnabled = ref(true)  // 优化开关，默认开启
const optimizeExpanded = ref(false)  // 优化项展开状态，默认收起

// 国家选择相关
interface GeoCountry {
  continent: {
    name: string
    name_zh_cn: string
  }
  country: {
    name: string
    name_zh_cn: string
  }
}

interface ContinentGroup {
  continentName: string
  continentNameZh: string
  countries: Array<{
    name: string
    nameZh: string
  }>
}

const geoCountries = ref<GeoCountry[]>([])
const continentGroups = ref<ContinentGroup[]>([])
const selectedCountries = ref<Set<string>>(new Set()) // 存储选中的国家英文名
const expandedContinents = ref<Set<string>>(new Set()) // 存储展开的大洲
const loadingCountries = ref(false)

let nextAvailId = 1
let nextCanvasId = 1

// 打开任务选择窗口并读取任务列表
async function openTaskSelection() {
  // 测试床模式：总是显示窗口让用户输入/确认聚类数量（不显示任务列表）
  if (environment.value === 'testbed') {
    showTaskSelectionModal.value = true
    selectedTaskIds.value = []
    return
  }
  
  // 实网模式：显示任务选择窗口
  showTaskSelectionModal.value = true
  selectedTaskIds.value = []
  await fetchTaskList()
}

// 获取任务列表
async function fetchTaskList() {
  loadingTasks.value = true
  try {
    const response = await fetch('/tasks')
    if (!response.ok) {
      throw new Error(`获取任务列表失败: ${response.status}`)
    }
    const tasks = await response.json()
    taskList.value = tasks
    console.log('任务列表:', tasks)
  } catch (error) {
    console.error('获取任务列表失败:', error)
    alert('获取任务列表失败: ' + (error instanceof Error ? error.message : '未知错误'))
    taskList.value = []
  } finally {
    loadingTasks.value = false
  }
}

// 格式化任务状态（将数字转换为百分比字符串）
function formatTaskStatus(status: number): string {
  return `${status.toFixed(1)}%`
}

// 根据任务状态百分比获取CSS类名
function getTaskStatusClass(status: number): string {
  if (status >= 100) {
    return 'completed'
  } else if (status > 0) {
    return 'running'
  } else {
    return 'failed'
  }
}

// 从选中的任务读取节点
async function loadNodesFromTasks() {
  // 验证聚类数量是否填写
  if (clusterCount.value === null || clusterCount.value === undefined || clusterCount.value < 1) {
    alert('请填写后端聚类数量（必须大于0）')
    return
  }
  
  // 测试床模式不需要选择任务，实网模式需要
  if (environment.value === 'realnet') {
  if (selectedTaskIds.value.length === 0) {
    alert('请至少选择一个任务')
    return
    }
  }
  
  loadingNodes.value = true
  let totalNodesAdded = 0
  
  try {
    let apiUrl: string
    let requestData: any
    
    // 根据环境类型选择不同的接口
    if (environment.value === 'testbed') {
      // 测试床模式：使用 /api/get_testbed_nodes，只传 cluster_count
      apiUrl = `/api/get_testbed_nodes`
      requestData = {
        cluster_count: clusterCount.value
      }
      console.log('测试床模式：正在读取测试床节点...')
    } else {
      // 实网模式：使用原来的接口，传 task_ids 和 cluster_count
      apiUrl = `/api/scan-results`
      requestData = {
        task_ids: selectedTaskIds.value,
        cluster_count: clusterCount.value
      }
      console.log(`实网模式：正在从 ${selectedTaskIds.value.length} 个任务读取节点...`)
    }
    
    console.log('请求URL:', apiUrl)
    console.log('请求数据:', requestData)
      
    const response = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify(requestData)
    })
    
      if (!response.ok) {
      // 尝试获取详细的错误信息
      const errorPrefix = environment.value === 'testbed' ? '读取测试床节点失败' : '读取任务结果失败'
      let errorMessage = `${errorPrefix}: ${response.status} ${response.statusText}`
      try {
        const errorData = await response.json()
        if (errorData.message || errorData.error || errorData.detail) {
          errorMessage += `\n详细信息: ${errorData.message || errorData.error || errorData.detail}`
        }
      } catch (e) {
        // 如果无法解析错误响应，使用默认错误信息
      }
      throw new Error(errorMessage)
    }
    
    const scanResults = await response.json()
    console.log('扫描结果:', scanResults)
    
    // scanResults 可能是单个任务的结果数组，或者是多个任务结果的对象
    // 需要统一处理成数组格式
    let allServerNodes: any[] = []
    
    if (Array.isArray(scanResults)) {
      // 如果返回的是数组，直接使用
      allServerNodes = scanResults
    } else if (typeof scanResults === 'object') {
      // 如果返回的是对象（可能按taskId分组），提取所有节点
      for (const taskId in scanResults) {
        const nodes = scanResults[taskId]
        if (Array.isArray(nodes)) {
          allServerNodes = allServerNodes.concat(nodes)
        }
      }
    }
    
    console.log(`共获取到 ${allServerNodes.length} 个节点`)
      
      // 将服务器节点转换为可用节点
    for (const serverNode of allServerNodes) {
      // 新数据格式：backend是数组
        const backendIps = serverNode.backend.map((b: any) => b.ip)
        
        const newNode: AvailableNode = {
          id: nextAvailId++,
          name: `节点-${serverNode.front_IP}`,
          backends: serverNode.backend.length,
          frontendIp: serverNode.front_IP,
          backendIps: backendIps,
          level: 1,
          // 保存服务器节点的完整参数
        rd_senstive_flage: serverNode.rd_senstive_flag,
          negative_cache: serverNode.negative_cache,
          frontend_query_merge_time_window: serverNode.frontend_query_merge_time_window,
          backendConfigs: serverNode.backend.map((b: any) => ({
            ip: b.ip,
            query_A_times: b.query_A_times,
            query_AAAA_times: b.query_AAAA_times,
            retry_total_time_window: b.retry_total_time_window,
            max_fanout_num: b.max_fanout_num,
          backend_query_merge_time_window: b.backend_query_merge_time_window,
          request_auth_times: b.request_auth_times,
          auth_time_index: b.auth_time_index,
          request_vic_times: b.request_vic_times,
          vic_time_index: b.vic_time_index,
          cluster_group: b.cluster_group !== undefined ? b.cluster_group : undefined
        })),
        // 新增的字段
        overall_auth_duration: serverNode.overall_auth_duration,
        overall_vic_duration: serverNode.overall_vic_duration,
        total_request_auth_times: serverNode.total_request_auth_times,
        total_request_vic_times: serverNode.total_request_vic_times
        }
        
        availableNodes.value.push(newNode)
        totalNodesAdded++
    }
    
    if (environment.value === 'testbed') {
      alert(`成功从测试床读取了 ${totalNodesAdded} 个节点`)
    } else {
    alert(`成功从 ${selectedTaskIds.value.length} 个任务中读取了 ${totalNodesAdded} 个节点`)
    }
    
    // 保存状态（包括 clusterCount）
    saveState().catch(err => console.error('保存状态失败:', err))
    
    showTaskSelectionModal.value = false
    selectedTaskIds.value = []
  } catch (error) {
    console.error('读取节点失败:', error)
    alert('读取节点失败: ' + (error instanceof Error ? error.message : '未知错误'))
  } finally {
    loadingNodes.value = false
  }
}

// 节点排序功能
type SortType = 'frontend_ip_asc' | 'frontend_ip_desc' | 'backend_count_asc' | 'backend_count_desc' | 'node_performance_asc' | 'node_performance_desc'

async function sortNodes(sortType: SortType) {
  if (sortingStatus.value === 'sorting') {
    return
  }

  if (!configLoaded.value) {
    alert('配置未加载，无法执行排序')
    return
  }

  sortingStatus.value = 'sorting'
  sortMessage.value = ''
  showSortMenu.value = false

  try {
    const config = await configService.getAttackChainConfig()
    const url = config.endpoints.sortNodes

    // 构建请求体
    const requestBody = {
      sort_type: sortType,
      timestamp: new Date().toISOString()
    }

    console.log('发送排序请求到:', url)
    console.log('排序类型:', sortType)
    console.log('请求体:', requestBody)

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify(requestBody),
      signal: createTimeoutSignal(config.timeout)
    })

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }

    const responseData = await response.json()
    console.log('排序响应:', responseData)

    if (responseData.success !== false) {
      // 检查响应中是否包含节点列表
      let nodesList: any[] = []
      
      if (responseData.nodes && Array.isArray(responseData.nodes)) {
        // 如果响应中有nodes字段
        nodesList = responseData.nodes
      } else if (responseData.data && Array.isArray(responseData.data)) {
        // 如果响应中有data字段
        nodesList = responseData.data
      } else if (Array.isArray(responseData)) {
        // 如果响应本身就是数组
        nodesList = responseData
      }

      if (nodesList.length > 0) {
        // 清空当前节点列表
        availableNodes.value = []
        
        // 重置ID计数器，从1开始
        nextAvailId = 1
        
        // 将服务器节点转换为可用节点并覆盖列表
        for (const serverNode of nodesList) {
          // 处理backend字段（可能是数组或对象）
          let backendIps: string[] = []
          let backendConfigs: BackendConfig[] = []
          
          if (Array.isArray(serverNode.backend)) {
            backendIps = serverNode.backend.map((b: any) => b.ip || b)
            backendConfigs = serverNode.backend.map((b: any) => ({
              ip: b.ip || b,
              query_A_times: b.query_A_times || 0,
              query_AAAA_times: b.query_AAAA_times || 0,
              retry_total_time_window: b.retry_total_time_window || null,
              max_fanout_num: b.max_fanout_num || null,
              backend_query_merge_time_window: b.backend_query_merge_time_window || null,
              request_auth_times: b.request_auth_times,
              auth_time_index: b.auth_time_index,
              request_vic_times: b.request_vic_times,
              vic_time_index: b.vic_time_index
            }))
          } else if (serverNode.backend_ips && Array.isArray(serverNode.backend_ips)) {
            backendIps = serverNode.backend_ips
          }

          const newNode: AvailableNode = {
            id: nextAvailId++,
            name: serverNode.name || `节点-${serverNode.front_IP || serverNode.frontend_ip || '未知'}`,
            backends: backendIps.length || serverNode.backends || 0,
            frontendIp: serverNode.front_IP || serverNode.frontend_ip,
            backendIps: backendIps,
            level: serverNode.level || 1,
            // 保存服务器节点的完整参数
            rd_senstive_flage: serverNode.rd_senstive_flag || serverNode.rd_senstive_flage,
            negative_cache: serverNode.negative_cache ?? null,
            frontend_query_merge_time_window: serverNode.frontend_query_merge_time_window ?? null,
            backendConfigs: backendConfigs.length > 0 ? backendConfigs : serverNode.backendConfigs,
            // 新增的字段
            overall_auth_duration: serverNode.overall_auth_duration,
            overall_vic_duration: serverNode.overall_vic_duration,
            total_request_auth_times: serverNode.total_request_auth_times,
            total_request_vic_times: serverNode.total_request_vic_times
          }
          
          availableNodes.value.push(newNode)
        }

        // 排序读取后，需要同步更新画布节点的 sourceAvailableNodeId
        // 因为节点ID会重新分配，需要根据 frontendIp 重新匹配
        canvasNodes.value.forEach(canvasNode => {
          if (canvasNode.sourceAvailableNodeId && canvasNode.frontendIp) {
            // 根据 frontendIp 在排序后的节点列表中查找对应的节点
            const matchedNode = availableNodes.value.find(
              node => node.frontendIp === canvasNode.frontendIp
            )
            if (matchedNode) {
              // 更新画布节点的 sourceAvailableNodeId 为新的节点ID
              canvasNode.sourceAvailableNodeId = matchedNode.id
              console.log(`更新画布节点 ${canvasNode.name} 的 sourceAvailableNodeId: ${matchedNode.id}`)
            } else {
              // 如果找不到匹配的节点，清除 sourceAvailableNodeId
              // 这可能是因为该节点在排序后的列表中不存在
              console.warn(`画布节点 ${canvasNode.name} (${canvasNode.frontendIp}) 在排序后的节点列表中未找到`)
              canvasNode.sourceAvailableNodeId = undefined
            }
          }
        })

        sortingStatus.value = 'success'
        sortMessage.value = `排序读取成功，已更新 ${availableNodes.value.length} 个节点`
        
        alert(`✅ 排序读取成功！\n\n已从服务器获取 ${availableNodes.value.length} 个排序后的节点\n排序方式：${getSortTypeLabel(sortType)}`)
      } else {
        // 如果没有节点列表，只提示排序成功
        sortingStatus.value = 'success'
        sortMessage.value = '排序命令已发送'
        alert('✅ 排序命令已发送，但响应中未包含节点列表')
      }
    } else {
      throw new Error(responseData.message || '排序失败')
    }
  } catch (error) {
    console.error('排序失败:', error)
    sortingStatus.value = 'error'
    sortMessage.value = error instanceof Error ? error.message : '排序失败'
    alert(`❌ 排序失败：${sortMessage.value}`)
  } finally {
    // 3秒后重置状态
    setTimeout(() => {
      sortingStatus.value = 'idle'
      sortMessage.value = ''
    }, 3000)
  }
}

// 创建超时信号
function createTimeoutSignal(timeout: number): AbortSignal {
  const controller = new AbortController()
  setTimeout(() => {
    controller.abort()
  }, timeout)
  return controller.signal
}

// 获取排序类型的中文显示名称
function getSortTypeLabel(sortType: SortType): string {
  switch (sortType) {
    case 'frontend_ip_asc':
      return '前端IP正序'
    case 'frontend_ip_desc':
      return '前端IP逆序'
    case 'backend_count_asc':
      return '后端数量正序'
    case 'backend_count_desc':
      return '后端数量逆序'
    case 'node_performance_asc':
      return '节点性能正序'
    case 'node_performance_desc':
      return '节点性能逆序'
    default:
      return '未知排序'
  }
}

// 关闭任务选择窗口
function closeTaskSelection() {
  showTaskSelectionModal.value = false
  selectedTaskIds.value = []
}

// 打开配置选项窗口
function openConfigModal() {
  showConfigModal.value = true
  configStatus.value = 'idle'
  configMessage.value = ''
  isConfigAndArrange.value = false // 仅配置，不执行编排
}

// 打开配置选项窗口（用于自动编排）
async function openConfigModalForArrange() {
  showConfigModal.value = true
  configStatus.value = 'idle'
  configMessage.value = ''
  isConfigAndArrange.value = true // 标记为配置后执行编排
  // 每次打开对话框时都重新加载国家列表
  await loadGeoCountries(true)
}

// 关闭配置选项窗口
function closeConfigModal() {
  showConfigModal.value = false
}

// 获取国家地理信息列表
async function loadGeoCountries(forceRefresh: boolean = false) {
  // 如果不是强制刷新且已经加载过，直接返回
  if (!forceRefresh && geoCountries.value.length > 0) {
    return
  }
  
  loadingCountries.value = true
  try {
    const response = await fetch('/api/now_node_geo', {
      method: 'GET',
      headers: {
        'Accept': 'application/json'
      }
    })
    
    if (!response.ok) {
      throw new Error(`获取国家列表失败: ${response.status} ${response.statusText}`)
    }
    
    const data = await response.json()
    geoCountries.value = data || []
    
    // 按大洲分组
    const groupsMap = new Map<string, ContinentGroup>()
    
    for (const item of geoCountries.value) {
      const continentName = item.continent.name
      const continentNameZh = item.continent.name_zh_cn || item.continent.name
      
      if (!groupsMap.has(continentName)) {
        groupsMap.set(continentName, {
          continentName,
          continentNameZh,
          countries: []
        })
      }
      
      const group = groupsMap.get(continentName)!
      group.countries.push({
        name: item.country.name,
        nameZh: item.country.name_zh_cn || item.country.name
      })
    }
    
    // 转换为数组并排序
    continentGroups.value = Array.from(groupsMap.values()).sort((a, b) => 
      a.continentNameZh.localeCompare(b.continentNameZh, 'zh-CN')
    )
    
    // 对每个大洲的国家进行排序
    continentGroups.value.forEach(group => {
      group.countries.sort((a, b) => 
        a.nameZh.localeCompare(b.nameZh, 'zh-CN')
      )
    })
    
    console.log('国家列表加载成功:', continentGroups.value)
  } catch (error) {
    console.error('获取国家列表失败:', error)
    alert(`获取国家列表失败: ${error instanceof Error ? error.message : '未知错误'}`)
  } finally {
    loadingCountries.value = false
  }
}

// 切换大洲展开/收起
function toggleContinent(continentName: string) {
  if (expandedContinents.value.has(continentName)) {
    expandedContinents.value.delete(continentName)
  } else {
    expandedContinents.value.add(continentName)
  }
}

// 切换国家选择
function toggleCountry(countryName: string) {
  if (selectedCountries.value.has(countryName)) {
    selectedCountries.value.delete(countryName)
  } else {
    selectedCountries.value.add(countryName)
  }
}

// 检查国家是否被选中
function isCountrySelected(countryName: string): boolean {
  return selectedCountries.value.has(countryName)
}

// 全选/取消全选某个大洲的所有国家
function toggleContinentCountries(continent: ContinentGroup) {
  const allSelected = continent.countries.every(c => selectedCountries.value.has(c.name))
  
  if (allSelected) {
    // 取消全选
    continent.countries.forEach(c => selectedCountries.value.delete(c.name))
  } else {
    // 全选
    continent.countries.forEach(c => selectedCountries.value.add(c.name))
  }
}

// 检查大洲是否全选
function isContinentAllSelected(continent: ContinentGroup): boolean {
  return continent.countries.length > 0 && 
         continent.countries.every(c => selectedCountries.value.has(c.name))
}

// 配置所有参数（自动编排配置 + 后端聚类配置）
async function applyAllConfigs() {
  if (configStatus.value === 'saving' || clusterStatus.value === 'saving') {
    return
  }
  
  const depth = configDepth.value ?? null
  const fanout = configFanout.value ?? null

  // 验证自动编排参数
  if (depth === null || depth < 1 || !Number.isInteger(depth)) {
    alert('请填写有效的"攻击链跳数"（正整数）')
    return
  }
  
  if (fanout === null || fanout < 1 || !Number.isInteger(fanout)) {
    alert('请填写有效的"攻击链最大度数"（正整数）')
    return
  }
  
  // 验证攻击链条数
  if (chainCount.value === null || chainCount.value < 1 || !Number.isInteger(chainCount.value)) {
    alert('请填写有效的"攻击链条数"（正整数）')
    return
  }

  // 验证后端聚类数量
  if (clusterCount.value === null || clusterCount.value < 1 || !Number.isInteger(clusterCount.value)) {
    alert('请先设置有效的"后端聚类数量"（正整数）')
    return
  }

  try {
    configStatus.value = 'saving'
    clusterStatus.value = 'saving'
    configMessage.value = '正在发送配置参数...'
    clusterMessage.value = ''

    const config = await configService.getConfig()
    const errors: string[] = []

    // 1. 发送自动编排配置
    try {
      // 确保参数是整数类型
      const maxDepth = Number.isInteger(depth) ? depth : Math.floor(Number(depth))
      const maxFanout = Number.isInteger(fanout) ? fanout : Math.floor(Number(fanout))
      const clusterCountValue = Number.isInteger(clusterCount.value) ? clusterCount.value : Math.floor(Number(clusterCount.value))
      const chainCountValue = Number.isInteger(chainCount.value) ? chainCount.value : Math.floor(Number(chainCount.value))
      
      // 根据环境类型构建不同的请求参数
      let autoArrangePayload: any
      let apiUrl: string
      
      if (environment.value === 'testbed') {
        // 测试床模式：使用新接口，不需要 country_name
        apiUrl = '/api/testbed_autopatch'
        autoArrangePayload = {
          action: 'update_auto_arrange',
          parameters: {
            maxDepth: maxDepth,
            maxFanout: maxFanout,
            cluster_count: clusterCountValue,
            chain_count: chainCountValue,
            target_ip: attackTarget.ip || '',
            victim_type: victimType.value,
            optimize: optimizeEnabled.value ? 'yes' : 'no'
          }
        }
  } else {
        // 实网模式：使用原接口，需要 country_name
        apiUrl = '/get_supplemented_csv'
        const selectedCountryNames = Array.from(selectedCountries.value)
        autoArrangePayload = {
          action: 'update_auto_arrange',
          parameters: {
            maxDepth: maxDepth,
            maxFanout: maxFanout,
            cluster_count: clusterCountValue,
            chain_count: chainCountValue,
            country_name: selectedCountryNames,
            target_ip: attackTarget.ip || '',
            victim_type: victimType.value,
            optimize: optimizeEnabled.value ? 'yes' : 'no'
          },
          timestamp: new Date().toISOString()
        }
      }

      console.log('发送自动编排配置:', autoArrangePayload)
      console.log('请求URL:', apiUrl)

      const autoArrangeResponse = await fetch(apiUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json'
        },
        body: JSON.stringify(autoArrangePayload),
        signal: createTimeoutSignal(config.attackChain.timeout)
      })

      if (!autoArrangeResponse.ok) {
        throw new Error(`HTTP error! status: ${autoArrangeResponse.status}`)
      }

      let autoArrangeResponseData: any = null
      try {
        autoArrangeResponseData = await autoArrangeResponse.json()
      } catch (error) {
        autoArrangeResponseData = null
      }

      console.log('自动编排配置响应:', autoArrangeResponseData)
      configStatus.value = 'success'
      configMessage.value = autoArrangeResponseData?.message || '自动编排配置已发送成功'
    } catch (error) {
      console.error('发送自动编排配置失败:', error)
      configStatus.value = 'error'
      configMessage.value = error instanceof Error ? error.message : '自动编排配置发送失败'
      errors.push('自动编排配置失败')
    }

    // 注意：后端聚类配置现在在"读取节点"时发送，不再在此处发送

    // 检查是否有错误
    if (errors.length > 0) {
      alert(`❌ 部分配置失败：\n${errors.join('\n')}`)
      return
    }

    // 所有配置成功，显示成功消息
    let successMessage = `✅ 配置已发送成功！\n\n`
    successMessage += `攻击链跳数: ${depth}\n`
    successMessage += `攻击链最大度数: ${fanout}\n`
    alert(successMessage)
  } catch (error) {
    console.error('配置失败:', error)
    configStatus.value = 'error'
    clusterStatus.value = 'error'
    const errorMsg = error instanceof Error ? error.message : '配置发送失败'
    configMessage.value = errorMsg
    clusterMessage.value = errorMsg
    alert(`❌ 配置发送失败：${errorMsg}`)
  } finally {
    setTimeout(() => {
      if (configStatus.value !== 'saving') {
        configStatus.value = 'idle'
      }
      if (clusterStatus.value !== 'saving') {
        clusterStatus.value = 'idle'
      }
    }, 3000)
  }
}

// 应用自动编排配置（保留用于兼容，现在调用 applyAllConfigs）
async function applyAutoArrangeConfig() {
  await applyAllConfigs()
}

// 执行自动编排（从原 autoArrangeNodes 函数中提取的核心逻辑）
async function executeAutoArrange() {
  // 先验证和发送配置参数
  const depth = configDepth.value ?? null
  const fanout = configFanout.value ?? null

  // 验证自动编排参数
  if (depth === null || depth < 1 || !Number.isInteger(depth)) {
    alert('请填写有效的"攻击链跳数"（正整数）')
    return
  }

  if (fanout === null || fanout < 1 || !Number.isInteger(fanout)) {
    alert('请填写有效的"攻击链最大度数"（正整数）')
    return
  }

  // 验证攻击链最大度数不能大于后端聚类数量
  if (clusterCount.value !== null && fanout > clusterCount.value) {
    alert(`攻击链最大度数不能大于后端聚类数量！\n\n当前设置：${fanout}\n后端聚类数量：${clusterCount.value}`)
    return
  }

  // 验证后端聚类数量
  if (clusterCount.value === null || clusterCount.value < 1 || !Number.isInteger(clusterCount.value)) {
    alert('请先设置有效的"后端聚类数量"（正整数）')
    return
  }

  try {
    // 先发送配置参数
    configStatus.value = 'saving'
    configMessage.value = '正在发送配置参数...'

    const config = await configService.getConfig()

    // 确保参数是整数类型
    const maxDepth = Number.isInteger(depth) ? depth : Math.floor(Number(depth))
    const maxFanout = Number.isInteger(fanout) ? fanout : Math.floor(Number(fanout))
    const clusterCountValue = Number.isInteger(clusterCount.value) ? clusterCount.value : Math.floor(Number(clusterCount.value))
  
    // 根据环境类型构建不同的请求参数
    let autoArrangePayload: any
    let apiUrl: string
    const chainCountValue = Number.isInteger(chainCount.value) ? chainCount.value : Math.floor(Number(chainCount.value))
    
    if (environment.value === 'testbed') {
      // 测试床模式：使用新接口，不需要 country_name
      apiUrl = '/api/testbed_autopatch'
      autoArrangePayload = {
        action: 'update_auto_arrange',
        parameters: {
          maxDepth: maxDepth,
          maxFanout: maxFanout,
          cluster_count: clusterCountValue,
          chain_count: chainCountValue,
          target_ip: attackTarget.ip || '',
          victim_type: victimType.value,
          optimize: optimizeEnabled.value ? 'yes' : 'no'
        }
      }
    } else {
      // 实网模式：使用原接口，需要 country_name
      apiUrl = '/get_supplemented_csv'
      const selectedCountryNames = Array.from(selectedCountries.value)
      autoArrangePayload = {
        action: 'update_auto_arrange',
        parameters: {
          maxDepth: maxDepth,
          maxFanout: maxFanout,
          cluster_count: clusterCountValue,
          chain_count: chainCountValue,
          country_name: selectedCountryNames,
          target_ip: attackTarget.ip || '',
          victim_type: victimType.value,
          optimize: optimizeEnabled.value ? 'yes' : 'no'
        },
        timestamp: new Date().toISOString()
      }
    }

    console.log('发送自动编排配置:', autoArrangePayload)
    console.log('请求URL:', apiUrl)
    console.log('请求方法: POST')
    console.log('请求体:', JSON.stringify(autoArrangePayload, null, 2))

    const autoArrangeResponse = await fetch(apiUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify(autoArrangePayload),
      signal: createTimeoutSignal(config.attackChain.timeout)
    })

    console.log('响应状态:', autoArrangeResponse.status, autoArrangeResponse.statusText)
    console.log('响应头:', Object.fromEntries(autoArrangeResponse.headers.entries()))

    if (!autoArrangeResponse.ok) {
      const errorText = await autoArrangeResponse.text().catch(() => '无法读取错误信息')
      console.error('响应错误内容:', errorText)
      
      // 尝试解析JSON格式的错误响应
      let parsedError: string = errorText
      try {
        const errorJson = JSON.parse(errorText)
        // 优先使用error字段，其次使用message字段，最后使用details字段
        if (errorJson.error) {
          parsedError = errorJson.error
        } else if (errorJson.message) {
          parsedError = errorJson.message
        } else if (errorJson.details) {
          parsedError = errorJson.details
        } else {
          parsedError = errorText
        }
      } catch (e) {
        // 如果不是JSON格式，使用原始文本
        parsedError = errorText
      }
      
      // 清理换行符和多余空格，使其在alert中正确显示
      parsedError = parsedError.replace(/\n\s+/g, '\n').trim()
      
      throw new Error(`HTTP error! status: ${autoArrangeResponse.status} ${autoArrangeResponse.statusText}\n\n${parsedError}`)
    }

    let autoArrangeResponseData: any = null
    try {
      autoArrangeResponseData = await autoArrangeResponse.json()
    } catch (error) {
      autoArrangeResponseData = null
    }

    console.log('自动编排配置响应:', autoArrangeResponseData)
    configStatus.value = 'success'
    configMessage.value = autoArrangeResponseData?.message || '自动编排配置已发送成功'

    // 配置发送成功后，关闭配置窗口
    if (showConfigModal.value) {
      closeConfigModal()
}

    // 等待一小段时间让用户看到配置成功的消息
    await new Promise(resolve => setTimeout(resolve, 500))

    // 然后继续执行编排逻辑
    if (availableNodes.value.length === 0) {
      alert('没有可用的节点进行编排，请先从服务器读取节点')
      return
    }
    
    // 获取连接关系数据
    let csvText: string = ''
    
    // 根据环境选择不同的接口获取连接关系
    if (environment.value === 'testbed') {
      // 测试床模式：使用 GET /api/testbed_autopatch 获取链结构
      const testbedCsvUrl = '/api/testbed_autopatch'
      console.log('测试床模式：从服务器获取测试床连接关系（GET /api/testbed_autopatch）')
      
      // 创建超时控制器
      const controller = new AbortController()
      const timeoutId = setTimeout(() => {
        console.log('获取CSV数据超时，正在中止请求...')
        controller.abort()
      }, config.attackChain.timeout)
      
      // 发送GET请求获取CSV数据
      const response = await fetch(testbedCsvUrl, {
        method: 'GET',
        headers: {
          'Accept': 'text/csv, text/plain'
        },
        signal: controller.signal
      })
      
      // 清除超时定时器
      clearTimeout(timeoutId)
      
      if (!response.ok) {
        throw new Error(`服务器响应错误: ${response.status} ${response.statusText}`)
      }
      
      // 获取CSV文本数据
      csvText = await response.text()
    } else {
      // 实网模式：使用 GET /get_supplemented_csv 获取链结构
      const supplementedCsvUrl = '/get_supplemented_csv'
      console.log('实网模式：从服务器获取实网连接关系（GET /get_supplemented_csv）')
    
    // 创建超时控制器
    const controller = new AbortController()
    const timeoutId = setTimeout(() => {
      console.log('获取CSV数据超时，正在中止请求...')
      controller.abort()
    }, config.attackChain.timeout)
    
    // 发送HTTP请求获取CSV数据
    const response = await fetch(supplementedCsvUrl, {
      method: 'GET',
      headers: {
        'Accept': 'text/csv, text/plain'
      },
      signal: controller.signal
    })
    
    // 清除超时定时器
    clearTimeout(timeoutId)
    
    if (!response.ok) {
      throw new Error(`服务器响应错误: ${response.status} ${response.statusText}`)
    }
    
    // 获取CSV文本数据
      csvText = await response.text()
    }
    
    if (!csvText || csvText.trim().length === 0) {
      throw new Error('服务器返回的CSV数据为空')
    }
    
    console.log('成功获取CSV数据，正在解析...')
    
    // 使用csvService解析CSV数据为三元组格式
    const tripleStructures = csvService['parseTripleCsvContent'](csvText)
    
    if (!tripleStructures || tripleStructures.length === 0) {
      throw new Error('解析CSV数据后没有找到有效的连接关系')
    }
    
    console.log(`成功解析 ${tripleStructures.length} 个连接关系`)
    
    // 收集所有可用节点的前端和后端IP
    const availableIPs = new Set<string>()
    availableNodes.value.forEach(node => {
      if (node.frontendIp) {
        availableIPs.add(node.frontendIp)
      }
      if (node.backendIps) {
        node.backendIps.forEach(ip => availableIPs.add(ip))
      }
    })
    
    console.log(`当前环境: ${environment.value}`)
    console.log(`节点列表中共有 ${availableNodes.value.length} 个节点`)
    console.log(`节点列表中共有 ${availableIPs.size} 个可用IP`)
    console.log(`节点列表中的前端IP:`, Array.from(availableIPs).slice(0, 10), availableIPs.size > 10 ? '...' : '')
    
    // 输出前几个节点的信息用于调试
    if (availableNodes.value.length > 0) {
      console.log('前3个节点信息:', availableNodes.value.slice(0, 3).map(n => ({
        name: n.name,
        frontendIp: n.frontendIp,
        backendIps: n.backendIps?.slice(0, 3)
      })))
    }
    
    // 辅助函数：将字符串或数组转换为数组
    const toArray = (value: string | string[]): string[] => {
      return Array.isArray(value) ? value : [value]
    }
    
    // 第一步：识别可能的攻击目标IP
    // 收集所有child_drs_in_ips中不在节点列表中的IP
    const potentialTargetIPs = new Map<string, number>() // IP -> 出现次数
    
    tripleStructures.forEach(triple => {
      const childIps = toArray(triple.child_drs_in_ips)
      childIps.forEach(childIp => {
        if (!availableIPs.has(childIp)) {
          potentialTargetIPs.set(childIp, (potentialTargetIPs.get(childIp) || 0) + 1)
        }
      })
    })
    
    // 找到出现最多的未知IP，很可能是攻击目标
    let detectedTargetIP: string | null = null
    let maxCount = 0
    
    potentialTargetIPs.forEach((count, ip) => {
      if (count > maxCount) {
        maxCount = count
        detectedTargetIP = ip
      }
    })
    
    if (detectedTargetIP) {
      console.log(`检测到可能的攻击目标IP: ${detectedTargetIP}（出现 ${maxCount} 次）`)
    }
    
    // 第二步：验证三元组，将连接到攻击目标的三元组也标记为有效
    const validTriples: TripleStructure[] = []
    const invalidTriples: Array<{triple: TripleStructure, unknownIPs: string[]}> = []
    
    tripleStructures.forEach(triple => {
      const unknownIPs: string[] = []
      
      // 检查parent_drs_in_ip（父节点必须在节点列表中）
      if (!availableIPs.has(triple.parent_drs_in_ip)) {
        unknownIPs.push(triple.parent_drs_in_ip)
      }
      
      // 检查parent_drs_backend_ip（父节点的后端IP必须在节点列表中）
      const backendIps = toArray(triple.parent_drs_backend_ip)
      backendIps.forEach(backendIp => {
        if (!availableIPs.has(backendIp)) {
          unknownIPs.push(backendIp)
      }
      })
      
      // 检查child_drs_in_ips（如果是攻击目标IP则允许）
      const childIps = toArray(triple.child_drs_in_ips)
      childIps.forEach(childIp => {
        if (!availableIPs.has(childIp) && childIp !== detectedTargetIP) {
          unknownIPs.push(childIp)
        }
      })
      
      if (unknownIPs.length > 0) {
        invalidTriples.push({ triple, unknownIPs })
        const backendStr = Array.isArray(triple.parent_drs_backend_ip) 
          ? triple.parent_drs_backend_ip.join(',') 
          : triple.parent_drs_backend_ip
        const childStr = Array.isArray(triple.child_drs_in_ips)
          ? triple.child_drs_in_ips.join(',')
          : triple.child_drs_in_ips
        console.warn(`跳过包含未知IP的连接: ${triple.parent_drs_in_ip} -> ${backendStr} -> ${childStr}`, '未知IP:', unknownIPs)
      } else {
        validTriples.push(triple)
      }
    })
    
    console.log(`验证完成: ${validTriples.length} 个有效连接, ${invalidTriples.length} 个无效连接`)
    
    // 如果检测到攻击目标IP，自动设置
    if (detectedTargetIP) {
      attackTarget.ip = detectedTargetIP
      console.log(`已自动设置攻击目标IP: ${detectedTargetIP}`)
    }
    
    if (validTriples.length === 0) {
      const envHint = environment.value === 'testbed' 
        ? `\n当前模式：测试床\n请确保后端返回的是测试床的连接关系，而不是实网的连接关系。\n` 
        : `\n当前模式：实网\n`
      alert(
        `自动编排失败！\n\n` +
        `所有 ${tripleStructures.length} 个连接关系都包含未知IP。\n` +
        `请确保服务器返回的IP地址在节点列表中存在。\n` +
        envHint +
        `\n提示：\n` +
        `1. 请先从服务器读取节点列表（当前节点数：${availableNodes.value.length}）\n` +
        `2. 然后再进行自动编排\n` +
        `3. 检查后端是否正确返回了${environment.value === 'testbed' ? '测试床' : '实网'}的连接关系`
      )
      return
    }
    
    // 清空当前画布
    const shouldClear = confirm(
      `从服务器获取到 ${tripleStructures.length} 个连接关系\n` +
      `其中 ${validTriples.length} 个有效，${invalidTriples.length} 个包含未知IP将被跳过。\n\n` +
      `是否清空当前画布并重新布置节点？\n` +
      `点击"确定"清空画布，点击"取消"保留现有节点。`
    )
    
    if (shouldClear) {
      canvasNodes.value = []
      connections.value = []
      targetManuallyMoved.value = false  // 重置攻击目标手动移动标记，允许自动定位
    }
    
    // 使用验证后的三元组和现有节点构建拓扑结构
    await buildTopologyFromExistingNodes(validTriples, detectedTargetIP)
    
    // 自动编排完成后，自动整理画布
    await resetCanvasView()
    
    // 生成详细的结果消息
    let resultMessage = `自动编排完成！\n\n`
    resultMessage += `✅ 成功生成:\n`
    resultMessage += `  - 有效连接: ${validTriples.length} 个\n`
    resultMessage += `  - 节点总数: ${canvasNodes.value.length} 个\n`
    resultMessage += `  - 连接总数: ${connections.value.length} 个\n`
    
    if (detectedTargetIP) {
      resultMessage += `\n🎯 自动识别攻击目标:\n`
      resultMessage += `  - IP地址: ${detectedTargetIP}\n`
    }
    
    if (invalidTriples.length > 0) {
      resultMessage += `\n❌ 生成失败: ${invalidTriples.length} 个\n`
      resultMessage += `  - 原因: 包含未知IP\n`
      if (environment.value === 'testbed') {
        resultMessage += `  - 当前模式: 测试床\n`
        resultMessage += `  - 提示: 请检查后端是否正确返回了测试床的连接关系\n`
      }
      resultMessage += `\n`
      
      // 显示前几个失败的示例
      const showCount = Math.min(3, invalidTriples.length)
      resultMessage += `失败示例（前${showCount}个）:\n`
      invalidTriples.slice(0, showCount).forEach((item, index) => {
        resultMessage += `${index + 1}. ${item.triple.parent_drs_in_ip} -> ${item.triple.parent_drs_backend_ip}\n`
        resultMessage += `   未知IP: ${item.unknownIPs.join(', ')}\n`
      })
      
      if (invalidTriples.length > showCount) {
        resultMessage += `   ... 还有 ${invalidTriples.length - showCount} 个失败的连接\n`
      }
      
      resultMessage += `\n提示: 请确保节点列表中包含所有需要的节点（当前节点数：${availableNodes.value.length}）`
    }
    
    alert(resultMessage)
    
  } catch (error) {
    console.error('自动编排失败:', error)
    configStatus.value = 'error'
    configMessage.value = error instanceof Error ? error.message : '自动编排失败'
    
    let errorMessage = '自动编排失败！\n\n'
    
    if (error instanceof Error) {
      if (error.name === 'AbortError') {
        // 超时错误
        const config = await configService.getConfig()
        errorMessage += `请求超时（${config.attackChain.timeout}ms）\n\n`
        errorMessage += '可能的原因：\n'
        errorMessage += '1. 服务器未启动或无响应\n'
        errorMessage += '2. 网络连接缓慢或不稳定\n'
        errorMessage += '3. 防火墙阻止了连接\n\n'
        errorMessage += `服务器地址: ${config.attackChain.protocol}://${config.attackChain.targetHost}:${config.attackChain.targetPort}\n`
        errorMessage += `端点: /get_supplemented_csv\n\n`
        errorMessage += '建议操作：\n'
        errorMessage += '- 检查服务器是否在运行\n'
        errorMessage += '- 使用浏览器访问上述地址测试连通性\n'
        errorMessage += '- 检查防火墙和网络设置'
      } else if (error.name === 'TypeError' && error.message.includes('fetch')) {
        // 网络错误
        errorMessage += '网络连接失败\n\n'
        errorMessage += '可能的原因：\n'
        errorMessage += '1. 服务器未启动\n'
        errorMessage += '2. 服务器地址配置错误\n'
        errorMessage += '3. 网络不可达\n'
        errorMessage += '4. CORS跨域问题\n\n'
        errorMessage += `错误详情: ${error.message}`
      } else {
        errorMessage += error.message
      }
    } else {
      errorMessage += '未知错误'
    }
    
    alert(errorMessage)
  } finally {
    // 3秒后重置状态
    setTimeout(() => {
      if (configStatus.value !== 'saving') {
        configStatus.value = 'idle'
        configMessage.value = ''
      }
    }, 3000)
  }
}

// 应用后端聚类配置
async function applyBackendClusterConfig() {
  if (clusterStatus.value === 'saving') {
    return
  }

  const clusterValue = clusterCount.value
  if (clusterValue === null || clusterValue < 1 || !Number.isInteger(clusterValue)) {
    alert('请填写有效的“后端聚类数量”（正整数）')
    return
  }

  try {
    clusterStatus.value = 'saving'
    clusterMessage.value = ''

    const config = await configService.getConfig()
    const payload = {
      action: 'update_backend_clustering',
      parameters: {
        clusterCount: clusterValue
      },
      timestamp: new Date().toISOString()
    }

    console.log('发送后端聚类配置:', payload)

    const response = await fetch('/api/auto-arrange/backend-cluster', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      },
      body: JSON.stringify(payload),
      signal: createTimeoutSignal(config.attackChain.timeout)
    })

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }

    let responseData: any = null
    try {
      responseData = await response.json()
    } catch (error) {
      responseData = null
    }

    console.log('后端聚类配置响应:', responseData)

    clusterStatus.value = 'success'
    clusterMessage.value = responseData?.message || '聚类配置已发送，等待后端处理'
    alert(`✅ 聚类配置已发送成功！\n\n后端聚类数量: ${clusterValue}`)
  } catch (error) {
    console.error('发送后端聚类配置失败:', error)
    clusterStatus.value = 'error'
    clusterMessage.value = error instanceof Error ? error.message : '聚类配置发送失败'
    alert(`❌ 聚类配置发送失败：${clusterMessage.value}`)
  } finally {
    setTimeout(() => {
      if (clusterStatus.value !== 'saving') {
        clusterStatus.value = 'idle'
      }
    }, 3000)
  }
}

function addAvailableNode() {
  // 打开弹出窗口进行完整配置
  modalNodeName.value = ''
  modalNodeBackends.value = 2
  modalNodeLevel.value = 1
  modalFrontendIp.value = ''
  modalBackendIps.value = Array(2).fill('')
  editingNodeId.value = null
  
  // 初始化高级参数为空
  modalRdSenstiveFlage.value = undefined
  modalNegativeCache.value = null
  modalFrontendQueryMergeTimeWindow.value = null
  modalOverallAuthDuration.value = undefined
  modalOverallVicDuration.value = undefined
  modalTotalRequestAuthTimes.value = undefined
  modalTotalRequestVicTimes.value = undefined
  
  // 重置展开状态
  showAdvancedParams.value = false
  showBackendConfigs.value = false
  
  // 初始化后端配置数组（空的配置）
  modalBackendConfigs.value = Array(2).fill(null).map(() => ({
    ip: '',
    query_A_times: 0,
    query_AAAA_times: 0,
    retry_total_time_window: null,
    max_fanout_num: null,
    backend_query_merge_time_window: null,
    request_auth_times: undefined,
    auth_time_index: undefined,
    request_vic_times: undefined,
    vic_time_index: undefined
  }))
  
  showModal.value = true
}

// 确认添加/编辑节点（从弹出窗口）
function confirmAddNode() {
  const name = modalNodeName.value.trim()
  const n = Math.max(1, Number(modalNodeBackends.value) || 1) // 移除8个的限制，支持任意数量的后端
  const level = Math.max(1, Number(modalNodeLevel.value) || 1)
  const frontendIp = modalFrontendIp.value.trim()
  const backendIps = modalBackendIps.value.map(ip => ip.trim()).filter(ip => ip !== '')
  
  if (!name) {
    alert('请输入节点名称')
    return
  }
  
  if (!frontendIp) {
    alert('请输入前端IP地址')
    return
  }
  
  if (backendIps.length !== n) {
    alert('请输入所有后端IP地址')
    return
  }
  
  // 同步后端IP到后端配置中
  modalBackendConfigs.value.forEach((config, idx) => {
    if (idx < backendIps.length) {
      config.ip = backendIps[idx]
    }
  })
  
  if (editingNodeId.value !== null) {
    // 编辑现有节点
    const nodeIndex = availableNodes.value.findIndex(n => n.id === editingNodeId.value)
    if (nodeIndex >= 0) {
      const oldNode = availableNodes.value[nodeIndex]
      const updatedNode: AvailableNode = {
        id: editingNodeId.value,
        name: name,
        backends: n,
        level: level,
        frontendIp: frontendIp,
        backendIps: backendIps,
        // 保存高级参数
        rd_senstive_flage: modalRdSenstiveFlage.value,
        negative_cache: modalNegativeCache.value,
        frontend_query_merge_time_window: modalFrontendQueryMergeTimeWindow.value,
        backendConfigs: modalBackendConfigs.value.length > 0 ? [...modalBackendConfigs.value] : undefined,
        // 保存统计字段
        overall_auth_duration: modalOverallAuthDuration.value,
        overall_vic_duration: modalOverallVicDuration.value,
        total_request_auth_times: modalTotalRequestAuthTimes.value,
        total_request_vic_times: modalTotalRequestVicTimes.value
      }
      
      // 更新可用节点列表
      availableNodes.value[nodeIndex] = updatedNode
      
      // 同步更新画布中的对应节点
      syncCanvasNodesWithAvailableNode(oldNode, updatedNode)
    }
    editingNodeId.value = null
    originalNodeData.value = null
  } else {
    // 添加新节点
    availableNodes.value.push({
      id: nextAvailId++,
      name: name,
      backends: n,
      level: level,
      frontendIp: frontendIp,
      backendIps: backendIps,
      // 保存高级参数
      rd_senstive_flage: modalRdSenstiveFlage.value,
      negative_cache: modalNegativeCache.value,
      frontend_query_merge_time_window: modalFrontendQueryMergeTimeWindow.value,
      backendConfigs: modalBackendConfigs.value.length > 0 ? [...modalBackendConfigs.value] : undefined,
      // 保存统计字段
      overall_auth_duration: modalOverallAuthDuration.value,
      overall_vic_duration: modalOverallVicDuration.value,
      total_request_auth_times: modalTotalRequestAuthTimes.value,
      total_request_vic_times: modalTotalRequestVicTimes.value
    })
  }
  
  // 重置表单和关闭弹窗
  modalNodeName.value = ''
  modalNodeBackends.value = 2
  modalNodeLevel.value = 1
  modalFrontendIp.value = ''
  modalBackendIps.value = Array(2).fill('')
  originalNodeData.value = null
  showModal.value = false
}

// 取消添加/编辑节点
function cancelAddNode() {
  // 如果是编辑模式，恢复原始数据
  if (editingNodeId.value !== null && originalNodeData.value) {
    modalNodeName.value = originalNodeData.value.name
    modalNodeLevel.value = originalNodeData.value.level
    modalFrontendIp.value = originalNodeData.value.frontendIp
    modalBackendIps.value = [...originalNodeData.value.backendIps]
    modalNodeBackends.value = originalNodeData.value.backends
    modalBackendConfigs.value = deepCopyBackendConfigs(originalNodeData.value.backendConfigs)
    modalRdSenstiveFlage.value = originalNodeData.value.rd_senstive_flage
    modalNegativeCache.value = originalNodeData.value.negative_cache ?? null
    modalFrontendQueryMergeTimeWindow.value = originalNodeData.value.frontend_query_merge_time_window ?? null
    modalOverallAuthDuration.value = originalNodeData.value.overall_auth_duration
    modalOverallVicDuration.value = originalNodeData.value.overall_vic_duration
    modalTotalRequestAuthTimes.value = originalNodeData.value.total_request_auth_times
    modalTotalRequestVicTimes.value = originalNodeData.value.total_request_vic_times
  }
  
  editingNodeId.value = null
  originalNodeData.value = null
  showModal.value = false
}

// 监听后端数量变化，动态调整后端IP输入框和后端配置
watch(modalNodeBackends, (newCount) => {
  const currentCount = modalBackendIps.value.length
  const n = Math.max(1, Number(newCount) || 1) // 移除8个的限制，支持任意数量的后端
  
  if (n > currentCount) {
    // 增加后端IP输入框
    for (let i = currentCount; i < n; i++) {
      modalBackendIps.value.push('')
      // 同时增加后端配置
      modalBackendConfigs.value.push({
        ip: '',
        query_A_times: 0,
        query_AAAA_times: 0,
        retry_total_time_window: null,
        max_fanout_num: null,
        backend_query_merge_time_window: null,
        request_auth_times: undefined,
        auth_time_index: undefined,
        request_vic_times: undefined,
        vic_time_index: undefined
      })
    }
  } else if (n < currentCount) {
    // 减少后端IP输入框
    modalBackendIps.value = modalBackendIps.value.slice(0, n)
    // 同时减少后端配置
    modalBackendConfigs.value = modalBackendConfigs.value.slice(0, n)
  }
})

// 深拷贝后端配置
function deepCopyBackendConfigs(configs: BackendConfig[]): BackendConfig[] {
  return configs.map(config => ({
    ip: config.ip,
    query_A_times: config.query_A_times,
    query_AAAA_times: config.query_AAAA_times,
    retry_total_time_window: config.retry_total_time_window,
    max_fanout_num: config.max_fanout_num,
    backend_query_merge_time_window: config.backend_query_merge_time_window,
    request_auth_times: config.request_auth_times,
    auth_time_index: config.auth_time_index,
    request_vic_times: config.request_vic_times,
    vic_time_index: config.vic_time_index,
    cluster_group: config.cluster_group
  }))
}

// 编辑可用节点
function editAvailableNode(node: AvailableNode) {
  editingNodeId.value = node.id
  modalNodeName.value = node.name
  modalNodeLevel.value = node.level || 1
  modalFrontendIp.value = node.frontendIp || ''
  
  // 先设置后端IP数组和配置，避免watch触发时数据不一致
  modalBackendIps.value = node.backendIps ? [...node.backendIps] : Array(node.backends).fill('')
  
  // 加载或初始化后端配置（深拷贝，避免直接修改原始数据）
  if (node.backendConfigs && node.backendConfigs.length > 0) {
    modalBackendConfigs.value = deepCopyBackendConfigs(node.backendConfigs)
  } else {
    // 如果没有后端配置，初始化空的配置数组
    modalBackendConfigs.value = Array(node.backends).fill(null).map((_, idx) => ({
      ip: node.backendIps?.[idx] || '',
      query_A_times: 0,
      query_AAAA_times: 0,
      retry_total_time_window: null,
      max_fanout_num: null,
      backend_query_merge_time_window: null,
      request_auth_times: undefined,
      auth_time_index: undefined,
      request_vic_times: undefined,
      vic_time_index: undefined,
      cluster_group: undefined
    }))
  }
  
  // 最后设置后端数量，这样watch触发时已经有正确的数据
  modalNodeBackends.value = node.backends
  
  // 加载高级参数
  modalRdSenstiveFlage.value = node.rd_senstive_flage
  modalNegativeCache.value = node.negative_cache ?? null
  modalFrontendQueryMergeTimeWindow.value = node.frontend_query_merge_time_window ?? null
  
  // 加载统计字段
  modalOverallAuthDuration.value = node.overall_auth_duration
  modalOverallVicDuration.value = node.overall_vic_duration
  modalTotalRequestAuthTimes.value = node.total_request_auth_times
  modalTotalRequestVicTimes.value = node.total_request_vic_times
  
  // 保存原始数据备份（深拷贝），用于取消时恢复
  originalNodeData.value = {
    name: node.name,
    level: node.level || 1,
    frontendIp: node.frontendIp || '',
    backendIps: node.backendIps ? [...node.backendIps] : [],
    backends: node.backends,
    backendConfigs: node.backendConfigs ? deepCopyBackendConfigs(node.backendConfigs) : [],
    rd_senstive_flage: node.rd_senstive_flage,
    negative_cache: node.negative_cache,
    frontend_query_merge_time_window: node.frontend_query_merge_time_window,
    overall_auth_duration: node.overall_auth_duration,
    overall_vic_duration: node.overall_vic_duration,
    total_request_auth_times: node.total_request_auth_times,
    total_request_vic_times: node.total_request_vic_times
  }
  
  showModal.value = true
}

// 删除可用节点
function deleteAvailableNode(nodeId: number) {
  if (confirm('确定要删除这个节点吗？这将从节点列表中永久删除该节点。')) {
    const index = availableNodes.value.findIndex(n => n.id === nodeId)
    if (index >= 0) {
      // 从可用节点列表中删除
      availableNodes.value.splice(index, 1)
      
      // 如果该节点已部署到画布，也从画布中移除
      const canvasIndex = canvasNodes.value.findIndex(n => n.sourceAvailableNodeId === nodeId)
      if (canvasIndex >= 0) {
        const canvasNodeId = canvasNodes.value[canvasIndex].id
        canvasNodes.value.splice(canvasIndex, 1)
        // 移除相关连接
        connections.value = connections.value.filter(c => c.from.nodeId !== canvasNodeId && c.to.nodeId !== canvasNodeId)
      }
    }
  }
}

// 自动编排节点（现在先打开配置窗口）
function autoArrangeNodes() {
  // 先打开配置窗口，配置完成后会自动执行编排
  openConfigModalForArrange()
}

// 同步画布节点与可用节点的信息
function syncCanvasNodesWithAvailableNode(oldNode: AvailableNode, updatedNode: AvailableNode) {
  // 查找画布中所有基于此可用节点创建的节点
  const canvasNodesToUpdate = canvasNodes.value.filter(canvasNode => {
    // 优先使用sourceAvailableNodeId进行匹配，如果没有则使用原有的匹配方式
    if (canvasNode.sourceAvailableNodeId !== undefined) {
      return canvasNode.sourceAvailableNodeId === updatedNode.id
    } else {
      // 兼容旧的匹配方式（用于已存在的画布节点）
      return canvasNode.name === oldNode.name && 
             canvasNode.backends === oldNode.backends &&
             canvasNode.frontendIp === oldNode.frontendIp
    }
  })
  
  // 更新匹配的画布节点
  canvasNodesToUpdate.forEach(canvasNode => {
    canvasNode.name = updatedNode.name
    canvasNode.backends = updatedNode.backends
    canvasNode.frontendIp = updatedNode.frontendIp
    canvasNode.backendIps = updatedNode.backendIps ? [...updatedNode.backendIps] : undefined
    canvasNode.sourceAvailableNodeId = updatedNode.id // 确保ID关联正确
    
    // 更新后端配置信息
    canvasNode.backendConfigs = updatedNode.backendConfigs ? [...updatedNode.backendConfigs] : undefined
    
    // 如果后端配置发生变化（包括聚类信息），重新计算后端分组
    if (updatedNode.backendConfigs && updatedNode.backendConfigs.length > 0) {
      const newBackendGroups = groupBackendsByCluster(updatedNode.backendConfigs)
      canvasNode.backendGroups = newBackendGroups.length > 0 ? newBackendGroups : undefined
    } else {
      canvasNode.backendGroups = undefined
    }
    
    // 如果后端数量发生变化，需要处理连接
    if (oldNode.backends !== updatedNode.backends) {
      handleBackendCountChange(canvasNode, oldNode.backends, updatedNode.backends)
    }
  })
}

// 处理后端数量变化时的连接调整
function handleBackendCountChange(canvasNode: CanvasNode, oldBackendCount: number, newBackendCount: number) {
  if (newBackendCount < oldBackendCount) {
    // 后端数量减少，移除多余的连接
    connections.value = connections.value.filter(conn => {
      if (conn.from.nodeId === canvasNode.id && conn.from.kind === 'backend') {
        const backendIndex = conn.from.backendIndex ?? 0
        return backendIndex < newBackendCount
      }
      return true
    })
  }
  // 如果后端数量增加，不需要特殊处理，新的后端端口会自动显示
}

const canvasRef = ref<HTMLDivElement | null>(null)
const canvasNodes = ref<CanvasNode[]>([])
const connections = ref<Connection[]>([])
const canvasWidth = ref(5000)  // 增大初始画布大小
const canvasHeight = ref(5000)  // 增大初始画布大小

// 创建节点ID到节点的映射，优化连接线渲染性能
const nodeMap = computed(() => {
  const map = new Map<number, CanvasNode>()
  canvasNodes.value.forEach(node => {
    map.set(node.id, node)
  })
  return map
})

// 计算连接线坐标（确保水平线也能正常显示）
const connectionLines = computed(() => {
  return connections.value.map((c, i) => {
    const fromNode = c.from.nodeId === 'target' ? null : nodeMap.value.get(c.from.nodeId as number) || null
    const toNode = c.to.nodeId === 'target' ? null : nodeMap.value.get(c.to.nodeId as number) || null
    
    const p1 = portPoint(fromNode, c.from)
    const p2 = portPoint(toNode, c.to)
    
    // 如果是完全水平的线（y1 === y2），添加微小偏移确保可见
    let y1 = p1.y
    let y2 = p2.y
    
    if (Math.abs(y1 - y2) < 0.5) {
      // 完全水平或几乎水平，添加0.5px偏移
      y2 = y1 + 0.5
    }
    
    return {
      key: `conn-${i}-${c.from.nodeId}-${c.from.backendIndex}-${c.to.nodeId}`,
      x1: p1.x,
      y1: y1,
      x2: p2.x,
      y2: y2,
      stroke: c.to.kind === 'target' ? 'url(#gradient-red)' : 'url(#gradient-purple)'
    }
  })
})

// 计算对称轴Y坐标（用于自激虚曲线）
const symmetryAxisY = computed(() => {
  if (canvasNodes.value.length === 0) return 0
  
  // 如果节点数量较少，直接使用所有节点的平均Y坐标作为对称轴
  if (canvasNodes.value.length <= 3) {
    const avgY = canvasNodes.value.reduce((sum, node) => sum + node.y + 25, 0) / canvasNodes.value.length
    return avgY
  }
  
  // 按层级分组节点
  const nodesByLevel = new Map<number, CanvasNode[]>()
  canvasNodes.value.forEach(node => {
    const level = node.level || 1
    if (!nodesByLevel.has(level)) {
      nodesByLevel.set(level, [])
    }
    nodesByLevel.get(level)!.push(node)
  })
  
  // 计算每层的总高度
  const levelHeights = new Map<number, number>()
  const nodeSpacing = 120
  const sortedLevels = Array.from(nodesByLevel.keys()).sort((a, b) => a - b)
  
  sortedLevels.forEach(level => {
    const nodes = nodesByLevel.get(level) || []
    const nodeCount = nodes.length
    const totalHeight = Math.max(0, (nodeCount - 1) * nodeSpacing)
    levelHeights.set(level, totalHeight)
  })
  
  // 找出最大高度，作为参考中心
  const maxHeight = Math.max(...Array.from(levelHeights.values()))
  return 200 + maxHeight / 2  // 画布中心Y坐标
})

// 通过连接关系向上追溯到根节点
function findRootNode(nodeId: number, visited: Set<number> = new Set()): CanvasNode | null {
  // 防止循环引用
  if (visited.has(nodeId)) {
    return null
  }
  visited.add(nodeId)
  
  const node = nodeMap.value.get(nodeId)
  if (!node) {
    return null
  }
  
  // 查找连接到该节点前端的连接（即该节点的父节点）
  const incomingConnections = connections.value.filter(conn => 
    conn.to.nodeId === nodeId && conn.to.kind === 'frontend'
  )
  
  // 如果没有连接到前端的连接，说明这是根节点
  if (incomingConnections.length === 0) {
    return node
  }
  
  // 递归向上查找（使用第一个父节点）
  const parentConnection = incomingConnections[0]
  if (parentConnection.from.nodeId === 'target' || parentConnection.from.nodeId === nodeId) {
    return node // 如果父节点是攻击目标或自己，返回当前节点
  }
  
  return findRootNode(parentConnection.from.nodeId as number, visited)
}

// 计算自激虚曲线（测试床模式且自激开启时）
const selfExcitationLines = computed(() => {
  // 只在测试床模式且自激开启时生成虚曲线
  if (environment.value !== 'testbed' || !selfExcitation.value) {
    return []
  }
  
  // 检查是否有连接到攻击靶标的连接，如果有连接，即使attackTarget.ip为空也应该显示虚线
  const hasConnectionToTarget = connections.value.some(conn => 
    conn.to.nodeId === 'target' && conn.to.kind === 'target'
  )
  
  if (canvasNodes.value.length === 0) {
    return []
  }
  
  // 如果没有连接到靶标的连接，且attackTarget.ip也为空，则不显示虚线
  if (!hasConnectionToTarget && !attackTarget.ip) {
    return []
  }
  
  const lines: Array<{
    key: string
    path: string
    fromNodeId: number
    backendIndex: number
  }> = []
  
  // 实时识别根节点（前端没有被连接的节点）
  const connectedFrontendNodeIds = new Set<number>()
  connections.value.forEach(conn => {
    if (conn.to.kind === 'frontend' && conn.to.nodeId !== 'target') {
      connectedFrontendNodeIds.add(conn.to.nodeId as number)
    }
  })
  const rootNodes = canvasNodes.value.filter(node => 
    !connectedFrontendNodeIds.has(node.id)
  )
  
  // 找到所有连接到攻击靶标的后端节点
  const connectionsToTarget = connections.value.filter(conn => 
    conn.to.nodeId === 'target' && conn.to.kind === 'target' &&
    conn.from.kind === 'backend' && conn.from.nodeId !== 'target'
  )
  
  if (connectionsToTarget.length === 0) {
    return []
  }
  
  // 如果没有根节点，但有待处理的连接，说明所有节点都是根节点（空白画布情况）
  // 在这种情况下，每个连接到靶标的节点本身就是根节点
  if (rootNodes.length === 0) {
    // 空白画布情况：所有节点都是根节点
    const axisY = symmetryAxisY.value
    
    connectionsToTarget.forEach((conn, index) => {
      const fromNode = nodeMap.value.get(conn.from.nodeId as number)
      if (!fromNode) return
      
      // 节点本身就是根节点
      const rootNode = fromNode
      
      // 判断后端节点相对对称轴的位置
      const nodeY = fromNode.y + 25 // 节点中心Y坐标（节点高度50，中心在25）
      const isAboveAxis = nodeY < axisY
      
      // 获取后端端口位置
      const backendPort = portPoint(fromNode, conn.from)
      
      // 获取根节点的前端端口位置
      const rootPort = portPoint(rootNode, { nodeId: rootNode.id, kind: 'frontend' })
      
      // 自环情况：从后端端口绕行回到前端端口
      const offsetY = isAboveAxis ? -150 : 150  // 绕行偏移量
      const loopRadius = 100
      
      // 使用两个控制点创建平滑的自环路径
      const controlPoint1X = backendPort.x + loopRadius
      const controlPoint1Y = backendPort.y + offsetY
      
      const controlPoint2X = rootPort.x - loopRadius
      const controlPoint2Y = rootPort.y + offsetY
      
      const path = `M ${backendPort.x} ${backendPort.y} C ${controlPoint1X} ${controlPoint1Y}, ${controlPoint2X} ${controlPoint2Y}, ${rootPort.x} ${rootPort.y}`
      
      lines.push({
        key: `self-excitation-${conn.from.nodeId}-${conn.from.backendIndex}-${index}`,
        path,
        fromNodeId: conn.from.nodeId as number,
        backendIndex: conn.from.backendIndex ?? 0
      })
    })
    
    return lines
  }
  
  // 计算对称轴Y坐标
  const axisY = symmetryAxisY.value
  
  // 为每个连接到攻击靶标的后端节点生成虚曲线到其根节点
  connectionsToTarget.forEach((conn, index) => {
    const fromNode = nodeMap.value.get(conn.from.nodeId as number)
    if (!fromNode) return
    
    // 通过连接关系向上追溯到根节点
    const rootNode = findRootNode(fromNode.id)
    if (!rootNode) {
      // 如果找不到根节点，说明节点本身就是根节点（空白画布情况）
      // 使用节点本身作为根节点
      const selfRootNode = fromNode
      
      // 判断后端节点相对对称轴的位置
      const nodeY = fromNode.y + 25
      const isAboveAxis = nodeY < axisY
      
      // 获取后端端口位置
      const backendPort = portPoint(fromNode, conn.from)
      
      // 获取根节点的前端端口位置
      const rootPort = portPoint(selfRootNode, { nodeId: selfRootNode.id, kind: 'frontend' })
      
      // 自环情况
      const offsetY = isAboveAxis ? -150 : 150
      const loopRadius = 100
      
      const controlPoint1X = backendPort.x + loopRadius
      const controlPoint1Y = backendPort.y + offsetY
      
      const controlPoint2X = rootPort.x - loopRadius
      const controlPoint2Y = rootPort.y + offsetY
      
      const path = `M ${backendPort.x} ${backendPort.y} C ${controlPoint1X} ${controlPoint1Y}, ${controlPoint2X} ${controlPoint2Y}, ${rootPort.x} ${rootPort.y}`
      
      lines.push({
        key: `self-excitation-${conn.from.nodeId}-${conn.from.backendIndex}-${index}`,
        path,
        fromNodeId: conn.from.nodeId as number,
        backendIndex: conn.from.backendIndex ?? 0
      })
      return
    }
    
    // 判断后端节点相对对称轴的位置
    const nodeY = fromNode.y + 25 // 节点中心Y坐标（节点高度50，中心在25）
    const isAboveAxis = nodeY < axisY
    
    // 获取后端端口位置
    const backendPort = portPoint(fromNode, conn.from)
    
    // 获取根节点的前端端口位置
    const rootPort = portPoint(rootNode, { nodeId: rootNode.id, kind: 'frontend' })
    
    // 如果根节点就是当前节点（自环情况），需要特殊处理
    const isSelfLoop = rootNode.id === fromNode.id
    
    // 计算绕行路径
    // 如果节点在对称轴上方，从上方绕行；否则从下方绕行
    const offsetY = isAboveAxis ? -150 : 150  // 绕行偏移量
    
    let path: string
    
    if (isSelfLoop) {
      // 自环情况：从后端端口绕行回到前端端口
      // 使用一个更大的绕行半径
      const loopRadius = 100
      
      // 使用两个控制点创建平滑的自环路径
      const controlPoint1X = backendPort.x + loopRadius
      const controlPoint1Y = backendPort.y + offsetY
      
      const controlPoint2X = rootPort.x - loopRadius
      const controlPoint2Y = rootPort.y + offsetY
      
      path = `M ${backendPort.x} ${backendPort.y} C ${controlPoint1X} ${controlPoint1Y}, ${controlPoint2X} ${controlPoint2Y}, ${rootPort.x} ${rootPort.y}`
    } else {
      // 正常情况：从后端端口到根节点前端端口
      // 使用贝塞尔曲线创建平滑的绕行路径
      // 起点：后端端口
      // 控制点1：向上/下偏移一定距离
      // 控制点2：接近根节点时向上/下偏移
      // 终点：根节点前端端口
      
      const controlPoint1X = backendPort.x + (rootPort.x - backendPort.x) * 0.3
      const controlPoint1Y = backendPort.y + offsetY
      
      const controlPoint2X = backendPort.x + (rootPort.x - backendPort.x) * 0.7
      const controlPoint2Y = rootPort.y + offsetY
      
      path = `M ${backendPort.x} ${backendPort.y} C ${controlPoint1X} ${controlPoint1Y}, ${controlPoint2X} ${controlPoint2Y}, ${rootPort.x} ${rootPort.y}`
    }
    
    lines.push({
      key: `self-excitation-${conn.from.nodeId}-${conn.from.backendIndex}-${index}`,
      path,
      fromNodeId: conn.from.nodeId as number,
      backendIndex: conn.from.backendIndex ?? 0
    })
  })
  
  return lines
})
const scale = ref(1)
const translateX = ref(0)
const translateY = ref(0)
const isDragging = ref(false)
const dragStart = ref({ x: 0, y: 0 })
const lastTranslate = ref({ x: 0, y: 0 })

// SVG使用固定的超大尺寸，支持负坐标
const SVG_SIZE = 20000  // 超大尺寸
const SVG_OFFSET = 10000  // 偏移量，让(0,0)在SVG中心

// 整理画布：重新排列节点为对称树形结构并调整视图
async function resetCanvasView() {
  if (!canvasRef.value) {
  scale.value = 1
  translateX.value = 0
  translateY.value = 0
    return
  }
  
  // 等待DOM更新
  await nextTick()
  
  // 如果有节点，先重新整理布局为对称树形结构
  if (canvasNodes.value.length > 0) {
    console.log('开始整理节点布局...')
    
    // 分析节点层级关系（基于连接关系）
    analyzeNodeLevels()
    
    // 执行自动布局，重新排列节点为对称树形结构
    await performAutoLayout()
    
    // 等待布局完成后的DOM更新
    await nextTick()
  }
  
  // 调整画布视图以完整显示攻击链
  const canvasRect = canvasRef.value.getBoundingClientRect()
  const canvasWidth = canvasRect.width
  const canvasHeight = canvasRect.height
  
  // 如果没有节点和攻击目标，重置到默认位置
  if (canvasNodes.value.length === 0 && !attackTarget.ip) {
    scale.value = 1
    translateX.value = 0
    translateY.value = 0
    return
  }
  
  // 节点尺寸
  const NODE_HEIGHT = 50
  const PADDING = 20  // 边缘边距
  
  // 识别根节点（前端没有被连接的节点）
  const connectedFrontendNodeIds = new Set<number>()
  connections.value.forEach(conn => {
    if (conn.to.kind === 'frontend' && conn.to.nodeId !== 'target') {
      connectedFrontendNodeIds.add(conn.to.nodeId as number)
    }
  })
  const rootNodes = canvasNodes.value.filter(node => 
    !connectedFrontendNodeIds.has(node.id)
  )
  
  // 计算所有内容的边界
  let leftmostX = Infinity
  let rightmostX = -Infinity
  let topmostY = Infinity
  let bottommostY = -Infinity
  
  // 计算节点的边界
  canvasNodes.value.forEach(node => {
    const nodeLeft = node.x
    const nodeRight = node.x + NODE_WIDTH
    const nodeTop = node.y
    const nodeBottom = node.y + NODE_HEIGHT
    
    leftmostX = Math.min(leftmostX, nodeLeft)
    rightmostX = Math.max(rightmostX, nodeRight)
    topmostY = Math.min(topmostY, nodeTop)
    bottommostY = Math.max(bottommostY, nodeBottom)
  })
  
  // 计算攻击目标的边界
  let targetCenterY = 0
  let targetCenterX = 0
  if (attackTarget.ip) {
    const switchHeight = 30  // 切换开关的高度
    const targetLeft = attackTarget.x
    const targetRight = attackTarget.x + TARGET_SIZE
    const targetTop = attackTarget.y
    const targetBottom = attackTarget.y + switchHeight + TARGET_SIZE
    
    targetCenterX = attackTarget.x + TARGET_SIZE / 2
    targetCenterY = attackTarget.y + (switchHeight + TARGET_SIZE) / 2
    
    leftmostX = Math.min(leftmostX, targetLeft)
    rightmostX = Math.max(rightmostX, targetRight)
    topmostY = Math.min(topmostY, targetTop)
    bottommostY = Math.max(bottommostY, targetBottom)
  }
  
  // 如果没有有效边界，重置到默认位置
  if (leftmostX === Infinity || rightmostX === -Infinity || topmostY === Infinity || bottommostY === -Infinity) {
    scale.value = 1
    translateX.value = 0
    translateY.value = 0
    return
  }
  
  // 计算根节点的平均中心Y坐标
  let rootNodesCenterY = 0
  let rootNodesLeftmostX = Infinity
  if (rootNodes.length > 0) {
    let rootNodesYSum = 0
    rootNodes.forEach(node => {
      rootNodesYSum += node.y + NODE_HEIGHT / 2
      rootNodesLeftmostX = Math.min(rootNodesLeftmostX, node.x)
    })
    rootNodesCenterY = rootNodesYSum / rootNodes.length
  } else {
    // 如果没有根节点，使用所有节点的平均Y坐标
    let nodesYSum = 0
    canvasNodes.value.forEach(node => {
      nodesYSum += node.y + NODE_HEIGHT / 2
    })
    rootNodesCenterY = canvasNodes.value.length > 0 ? nodesYSum / canvasNodes.value.length : (topmostY + bottommostY) / 2
    rootNodesLeftmostX = leftmostX
  }
  
  // 计算可用的画布尺寸（减去右侧节点列表和底部菜单栏的宽度）
  const sidebarWidth = 280  // 右侧节点列表宽度
  const bottomBarHeight = 60  // 底部菜单栏高度
  const availableWidth = canvasWidth - sidebarWidth
  const availableHeight = canvasHeight - bottomBarHeight
  
  // 计算水平距离（从根节点到攻击目标）
  let horizontalDistance = 0
  if (attackTarget.ip && rootNodes.length > 0) {
    // 根节点左边缘到攻击目标中心（或右边缘）的距离
    // 使用根节点左边缘到攻击目标中心的距离，确保两者都能紧贴屏幕边缘
    horizontalDistance = targetCenterX - rootNodesLeftmostX
  } else if (attackTarget.ip) {
    // 如果没有根节点，使用最左边到攻击目标的距离
    horizontalDistance = targetCenterX - leftmostX
  } else if (rootNodes.length > 0) {
    // 如果没有攻击目标，使用根节点到最右边的距离
    horizontalDistance = rightmostX - rootNodesLeftmostX
  } else {
    // 如果没有根节点和攻击目标，使用内容宽度
    horizontalDistance = rightmostX - leftmostX
  }
  
  // 计算缩放比例
  // 只根据水平方向（根节点到攻击目标）来调整缩放，不考虑垂直方向
  // 目标：根节点紧贴左侧边缘，攻击目标紧贴右侧边缘
  const newScale = horizontalDistance > 0 
    ? (availableWidth - PADDING * 2) / horizontalDistance
    : 1
  // 限制最大缩放为1，避免放大
  const finalScale = Math.min(newScale, 1)
  
  // 计算平移位置
  // 目标：根节点紧贴左侧边缘，攻击目标紧贴右侧边缘，两者都垂直居中
  const targetScreenCenterY = availableHeight / 2
  
  // 计算根节点在屏幕上的Y坐标（缩放后）
  const scaledRootNodesCenterY = rootNodesCenterY * finalScale
  
  // 计算攻击目标在屏幕上的Y坐标（缩放后）
  const scaledTargetCenterY = attackTarget.ip ? targetCenterY * finalScale : scaledRootNodesCenterY
  
  // 计算根节点在屏幕上的X坐标（缩放后）
  const scaledRootNodesLeftmostX = rootNodesLeftmostX * finalScale
  
  // 计算攻击目标在屏幕上的X坐标（缩放后）
  const scaledTargetCenterX = attackTarget.ip ? targetCenterX * finalScale : 0
  
  // 水平平移：使根节点紧贴左边缘，攻击目标紧贴右边缘
  let newTranslateX = 0
  if (attackTarget.ip && rootNodes.length > 0) {
    // 同时考虑根节点和攻击目标的位置
    // 目标：根节点在左侧（PADDING），攻击目标在右侧（availableWidth - PADDING）
    const targetRightEdgeX = availableWidth - PADDING
    const targetLeftEdgeX = PADDING
    
    // 先让根节点紧贴左边缘
    newTranslateX = targetLeftEdgeX - scaledRootNodesLeftmostX
    
    // 计算攻击目标在平移后的位置
    const targetAfterTranslateX = scaledTargetCenterX + newTranslateX
    
    // 如果攻击目标不在右边缘，调整平移使其紧贴右边缘
    // 这样根节点和攻击目标都会紧贴各自的边缘
    if (targetAfterTranslateX !== targetRightEdgeX) {
      // 重新计算，确保攻击目标紧贴右边缘
      newTranslateX = targetRightEdgeX - scaledTargetCenterX
    }
  } else if (rootNodes.length > 0) {
    // 只有根节点，根节点紧贴左边缘
    newTranslateX = PADDING - scaledRootNodesLeftmostX
  } else if (attackTarget.ip) {
    // 只有攻击目标，攻击目标紧贴右边缘
    const targetRightEdgeX = availableWidth - PADDING
    newTranslateX = targetRightEdgeX - scaledTargetCenterX
  } else {
    // 既没有根节点也没有攻击目标，使用默认位置
    newTranslateX = PADDING - leftmostX * finalScale
  }
  
  // 垂直平移：使根节点和攻击目标的中心都垂直居中
  // 如果两者都存在，取它们的平均中心；否则使用根节点的中心
  let targetVerticalCenter = scaledRootNodesCenterY
  if (attackTarget.ip && rootNodes.length > 0) {
    // 取根节点和攻击目标的平均中心Y坐标
    targetVerticalCenter = (scaledRootNodesCenterY + scaledTargetCenterY) / 2
  } else if (attackTarget.ip) {
    // 只有攻击目标，使用攻击目标的中心
    targetVerticalCenter = scaledTargetCenterY
  } else if (rootNodes.length > 0) {
    // 只有根节点，使用根节点的中心
    targetVerticalCenter = scaledRootNodesCenterY
  } else {
    // 既没有根节点也没有攻击目标，使用内容中心
    const contentCenterY = (topmostY + bottommostY) / 2 * finalScale
    targetVerticalCenter = contentCenterY
  }
  
  // 调整垂直平移，使得目标中心在屏幕中央
  const newTranslateY = targetScreenCenterY - targetVerticalCenter
  
  // 应用变换
  scale.value = finalScale
  translateX.value = newTranslateX
  translateY.value = newTranslateY
  
  console.log('画布整理完成:', {
    scale: finalScale,
    translateX: newTranslateX,
    translateY: newTranslateY,
    horizontalDistance,
    rootNodesLeftmostX,
    targetCenterX: attackTarget.ip ? targetCenterX : null,
    contentBounds: { leftmostX, rightmostX, topmostY, bottommostY },
    canvasSize: { width: canvasWidth, height: canvasHeight },
    availableSize: { width: availableWidth, height: availableHeight }
  })
}

// 分析节点层级关系（基于连接关系）
function analyzeNodeLevels() {
  // 识别根节点（前端没有被连接的节点）
  const connectedFrontendNodeIds = new Set<number>()
  connections.value.forEach(conn => {
    if (conn.to.kind === 'frontend' && conn.to.nodeId !== 'target') {
      connectedFrontendNodeIds.add(conn.to.nodeId as number)
    }
  })
  
  const rootNodes = canvasNodes.value.filter(node => 
    !connectedFrontendNodeIds.has(node.id)
  )
  
  // 如果所有节点都有连接，尝试使用已有的level属性
  if (rootNodes.length === 0) {
    console.log('未找到根节点，使用已有层级信息')
    return
  }
  
  // 从根节点开始，使用BFS分配层级
  const levelMap = new Map<number, number>()
  const visited = new Set<number>()
  const queue: Array<{ nodeId: number; level: number }> = []
  
  // 初始化根节点
  rootNodes.forEach(node => {
    levelMap.set(node.id, 1)
    visited.add(node.id)
    queue.push({ nodeId: node.id, level: 1 })
  })
  
  // BFS遍历
  while (queue.length > 0) {
    const { nodeId, level } = queue.shift()!
    
    // 找到所有从该节点出发的连接
    const outgoingConnections = connections.value.filter(conn => 
      conn.from.nodeId === nodeId && 
      conn.to.kind === 'frontend' && 
      conn.to.nodeId !== 'target'
    )
    
    outgoingConnections.forEach(conn => {
      const childNodeId = conn.to.nodeId as number
      if (!visited.has(childNodeId)) {
        const childLevel = level + 1
        levelMap.set(childNodeId, childLevel)
        visited.add(childNodeId)
        queue.push({ nodeId: childNodeId, level: childLevel })
      }
    })
  }
  
  // 更新节点的层级
  canvasNodes.value.forEach(node => {
    if (levelMap.has(node.id)) {
      node.level = levelMap.get(node.id)!
    } else if (!node.level) {
      // 如果没有找到层级，默认设为1
      node.level = 1
    }
  })
  
  console.log(`节点层级分析完成，共 ${levelMap.size} 个节点分配了层级`)
}

// 节点拖拽部署相关
const draggingNodeDeploy = ref<{ node: AvailableNode | null; x: number; y: number }>({ node: null, x: 0, y: 0 })

// 开始拖拽节点进行部署
function startDragDeploy(e: MouseEvent, node: AvailableNode) {
  // 检查节点是否已部署
  const isDeployed = canvasNodes.value.some(cn => cn.sourceAvailableNodeId === node.id)
  if (isDeployed) {
    e.preventDefault()
    return  // 已部署的节点不能再次拖拽部署
  }
  
  e.preventDefault()
  draggingNodeDeploy.value = { node, x: e.clientX, y: e.clientY }
  
  window.addEventListener('mousemove', onDragDeployMove)
  window.addEventListener('mouseup', onDragDeployEnd)
}

// 拖拽过程中更新预览位置
function onDragDeployMove(e: MouseEvent) {
  if (!draggingNodeDeploy.value.node) return
  draggingNodeDeploy.value.x = e.clientX
  draggingNodeDeploy.value.y = e.clientY
}

// 结束拖拽，部署节点
function onDragDeployEnd(e: MouseEvent) {
  window.removeEventListener('mousemove', onDragDeployMove)
  window.removeEventListener('mouseup', onDragDeployEnd)
  
  if (!draggingNodeDeploy.value.node || !canvasRef.value) {
    draggingNodeDeploy.value = { node: null, x: 0, y: 0 }
    return
  }
  
  // 检查鼠标是否在画布内
  const canvasRect = canvasRef.value.getBoundingClientRect()
  if (e.clientX >= canvasRect.left && e.clientX <= canvasRect.right &&
      e.clientY >= canvasRect.top && e.clientY <= canvasRect.bottom) {
    
    // 获取 canvas-content 的实际位置（已应用 transform）
    const canvasContent = canvasRef.value.querySelector('.canvas-content') as HTMLElement
    if (!canvasContent) return
    
    const contentRect = canvasContent.getBoundingClientRect()
    
    // 将屏幕坐标转换为 canvas-content 内部坐标
    const canvasX = (e.clientX - contentRect.left) / scale.value
    const canvasY = (e.clientY - contentRect.top) / scale.value
    
    // 根据聚类信息创建后端分组
    const backendGroups = groupBackendsByCluster(draggingNodeDeploy.value.node.backendConfigs)
    
    // 创建画布节点（节点左上角位置 = 鼠标位置 - 节点尺寸的一半）
    const newNode: CanvasNode = {
      id: nextCanvasId++,
      name: draggingNodeDeploy.value.node.name,
      backends: draggingNodeDeploy.value.node.backends,
      x: canvasX - NODE_WIDTH / 2,  // 居中放置
      y: canvasY - 25,  // 居中放置（节点高度50px的一半）
      frontendIp: draggingNodeDeploy.value.node.frontendIp,
      backendIps: draggingNodeDeploy.value.node.backendIps,
      level: draggingNodeDeploy.value.node.level,
      sourceAvailableNodeId: draggingNodeDeploy.value.node.id,
      backendExpanded: false,  // 默认不展开
      backendGroups: backendGroups.length > 0 ? backendGroups : undefined,  // 如果有聚类信息，设置分组
      isAutoArranged: false,  // 手动拖拽的节点
      // 复制服务器节点的额外参数
      rd_senstive_flage: draggingNodeDeploy.value.node.rd_senstive_flage,
      negative_cache: draggingNodeDeploy.value.node.negative_cache,
      frontend_query_merge_time_window: draggingNodeDeploy.value.node.frontend_query_merge_time_window,
      backendConfigs: draggingNodeDeploy.value.node.backendConfigs,
      // 复制统计字段
      overall_auth_duration: draggingNodeDeploy.value.node.overall_auth_duration,
      overall_vic_duration: draggingNodeDeploy.value.node.overall_vic_duration,
      total_request_auth_times: draggingNodeDeploy.value.node.total_request_auth_times,
      total_request_vic_times: draggingNodeDeploy.value.node.total_request_vic_times
    }
    
    canvasNodes.value.push(newNode)
  }
  
  draggingNodeDeploy.value = { node: null, x: 0, y: 0 }
}

// 攻击目标
const attackTarget = reactive({
  ip: '',
  port: 80,
  x: 0,
  y: 0
})

// 受攻击类型：'dns' | 'non-dns'
const victimType = ref<'dns' | 'non-dns'>('non-dns')

// 攻击目标尺寸常量
const TARGET_SIZE = 60
const TARGET_MARGIN = 20

// 攻击目标是否被手动移动过
const targetManuallyMoved = ref(false)

// 更新攻击目标位置
function updateTargetPosition() {
  // 如果已经被手动移动过，就不再自动更新位置
  if (targetManuallyMoved.value) return
  
  // 获取画布可视区域的尺寸
  if (!canvasRef.value) {
    attackTarget.x = 700
    attackTarget.y = 400
    return
  }
  
  const canvasRect = canvasRef.value.getBoundingClientRect()
  const visibleWidth = canvasRect.width
  const visibleHeight = canvasRect.height
  
  // 将攻击目标放在可视区域的右下角
  attackTarget.x = visibleWidth - TARGET_SIZE - TARGET_MARGIN - 250  // 减去右侧节点列表宽度
  attackTarget.y = visibleHeight - TARGET_SIZE - TARGET_MARGIN - 60 - 30  // 减去底部栏高度
}

// 攻击目标拖拽相关
const draggingTarget = ref(false)
const targetDragStart = ref({ x: 0, y: 0, targetX: 0, targetY: 0 })
const targetHasMoved = ref(false) // 标记是否发生了实际移动
let targetAbortController: AbortController | null = null

function onTargetMouseDown(e: MouseEvent) {
  // 如果点击的是输入框，不触发拖拽
  if ((e.target as HTMLElement).tagName === 'INPUT') return
  
  e.stopPropagation()
  e.preventDefault()  // 防止触发其他默认行为
  
  // 如果之前的拖拽还没结束，先清理
  if (targetAbortController) {
    targetAbortController.abort()
  }
  
  draggingTarget.value = true
  targetHasMoved.value = false // 重置移动标志
  
  const canvasRect = canvasRef.value?.getBoundingClientRect()
  if (!canvasRect) return
  
  // 记录拖拽开始时的鼠标位置和目标位置
  targetDragStart.value = {
    x: e.clientX,
    y: e.clientY,
    targetX: attackTarget.x,
    targetY: attackTarget.y
  }
  
  // 使用 AbortController 来管理事件监听器
  targetAbortController = new AbortController()
  const signal = targetAbortController.signal
  
  // 使用 document 而不是 window，并添加 capture: true 确保优先捕获
  document.addEventListener('mousemove', onTargetMouseMove, { signal, capture: true })
  document.addEventListener('mouseup', onTargetMouseUp, { signal, capture: true })
}

function onTargetMouseMove(e: MouseEvent) {
  // 检查 AbortController 是否存在
  if (!targetAbortController) return
  
  if (!draggingTarget.value) {
    // 强制终止
    if (targetAbortController) {
      targetAbortController.abort()
      targetAbortController = null
    }
    return
  }
  
  e.preventDefault()
  e.stopPropagation()
  
  // 计算鼠标移动的距离（屏幕坐标）
  const deltaX = e.clientX - targetDragStart.value.x
  const deltaY = e.clientY - targetDragStart.value.y
  
  // 转换为画布坐标的偏移量
  const canvasDeltaX = deltaX / scale.value
  const canvasDeltaY = deltaY / scale.value
  
  // 更新攻击目标位置
  attackTarget.x = targetDragStart.value.targetX + canvasDeltaX
  attackTarget.y = targetDragStart.value.targetY + canvasDeltaY
  
  // 如果移动距离超过阈值（5像素），标记为已移动
  const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
  if (distance > 5) {
    targetHasMoved.value = true
    targetManuallyMoved.value = true // 标记为手动移动过
  }
}

function onTargetMouseUp(_e: MouseEvent) {
  const hadMoved = targetHasMoved.value
  
  draggingTarget.value = false
  
  // 使用 AbortController 移除所有监听器
  if (targetAbortController) {
    targetAbortController.abort()
    targetAbortController = null
  }
  
  // 如果发生了移动，标记为已移动（用于阻止点击事件）
  // 延迟重置，让点击事件有机会检查
  if (hadMoved) {
    setTimeout(() => {
      targetHasMoved.value = false
    }, 200)
  } else {
    // 没有发生移动，立即重置，允许点击事件触发
    targetHasMoved.value = false
  }
}

function onTargetClick(e: MouseEvent) {
  // 如果刚刚发生了拖拽移动，不触发点击效果
  if (targetHasMoved.value) {
    e.stopPropagation()
    return
  }
  // 跳转到攻击效果评估页面
  if (navigateToAttackEffect && typeof navigateToAttackEffect === 'function') {
    navigateToAttackEffect(attackTarget.ip)
  }
}

// 状态持久化相关
const STORAGE_KEY_BASE = 'topology-designer-state'
const ENVIRONMENT_STORAGE_KEY = 'topology-designer-environment' // 单独存储环境模式
const IDB_DB_NAME = 'topology-designer-db'
const IDB_STORE_NAME = 'topology-state'
const STORAGE_MARKER_KEY = 'topology-designer-storage-marker' // 存储位置标记

// 获取指定环境的存储键
function getStorageKey(env?: 'testbed' | 'realnet'): string {
  const envValue = env || environment.value
  return `${STORAGE_KEY_BASE}-${envValue}`
}

// IndexedDB 工具函数
async function openIndexedDB(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(IDB_DB_NAME, 1)
    
    request.onerror = () => {
      reject(new Error('无法打开 IndexedDB'))
    }
    
    request.onsuccess = () => {
      resolve(request.result)
    }
    
    request.onupgradeneeded = (event) => {
      const db = (event.target as IDBOpenDBRequest).result
      if (!db.objectStoreNames.contains(IDB_STORE_NAME)) {
        db.createObjectStore(IDB_STORE_NAME)
      }
    }
  })
}

// 深度清理数据，移除不可序列化的内容
function sanitizeDataForStorage(data: any): any {
  try {
    // 先尝试 JSON 序列化/反序列化，这样可以移除所有不可序列化的内容
    const jsonString = JSON.stringify(data)
    return JSON.parse(jsonString)
  } catch (error: any) {
    console.warn('JSON 序列化失败，尝试深度清理:', error.message)
    // 如果 JSON 序列化失败，进行深度清理
    return deepSanitize(data)
  }
}

// 深度清理函数，递归移除不可序列化的内容
function deepSanitize(obj: any, visited = new WeakSet()): any {
  // 处理 null 和 undefined
  if (obj === null || obj === undefined) {
    return obj
  }
  
  // 处理基本类型
  if (typeof obj !== 'object') {
    return obj
  }
  
  // 防止循环引用
  if (visited.has(obj)) {
    return null
  }
  visited.add(obj)
  
  // 处理数组
  if (Array.isArray(obj)) {
    return obj.map(item => deepSanitize(item, visited))
  }
  
  // 处理 Date
  if (obj instanceof Date) {
    return obj.toISOString()
  }
  
  // 处理 Map
  if (obj instanceof Map) {
    const result: any = {}
    obj.forEach((value, key) => {
      result[String(key)] = deepSanitize(value, visited)
    })
    return result
  }
  
  // 处理 Set
  if (obj instanceof Set) {
    return Array.from(obj).map(item => deepSanitize(item, visited))
  }
  
  // 处理普通对象
  const result: any = {}
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      try {
        const value = obj[key]
        // 跳过函数和 Symbol
        if (typeof value === 'function' || typeof value === 'symbol') {
          continue
        }
        result[key] = deepSanitize(value, visited)
      } catch (e) {
        // 跳过无法访问的属性
        continue
      }
    }
  }
  return result
}

// 使用 IndexedDB 保存数据
async function saveToIndexedDB(key: string, data: any): Promise<void> {
  try {
    // 先清理数据，确保可序列化
    const sanitizedData = sanitizeDataForStorage(data)
    
    const db = await openIndexedDB()
    return new Promise((resolve, reject) => {
      const transaction = db.transaction([IDB_STORE_NAME], 'readwrite')
      const store = transaction.objectStore(IDB_STORE_NAME)
      
      // 保存清理后的数据
      const request = store.put(sanitizedData, key)
      
      request.onsuccess = () => {
        console.log(`数据已保存到 IndexedDB: ${key}`)
        // 在 localStorage 中保存标记，指示数据存储在 IndexedDB
        try {
          const markers = JSON.parse(localStorage.getItem(STORAGE_MARKER_KEY) || '{}')
          markers[key] = 'indexeddb'
          localStorage.setItem(STORAGE_MARKER_KEY, JSON.stringify(markers))
        } catch (e) {
          // 忽略标记保存失败
        }
        resolve()
      }
      
      request.onerror = (event) => {
        const error = (event.target as IDBRequest).error
        console.error('IndexedDB 保存错误:', error)
        reject(new Error(`保存到 IndexedDB 失败: ${error?.message || '未知错误'}`))
      }
      
      transaction.onerror = (event) => {
        const error = (event.target as IDBTransaction).error
        console.error('IndexedDB 事务错误:', error)
        reject(new Error(`IndexedDB 事务失败: ${error?.message || '未知错误'}`))
      }
    })
  } catch (error: any) {
    console.error('IndexedDB 保存异常:', error)
    throw error
  }
}

// 从 IndexedDB 加载数据
async function loadFromIndexedDB(key: string): Promise<any | null> {
  try {
    const db = await openIndexedDB()
    return new Promise((resolve, reject) => {
      const transaction = db.transaction([IDB_STORE_NAME], 'readonly')
      const store = transaction.objectStore(IDB_STORE_NAME)
      const request = store.get(key)
      
      request.onsuccess = () => {
        const result = request.result
        if (result) {
          console.log(`从 IndexedDB 成功加载数据: ${key}`)
        }
        resolve(result || null)
      }
      
      request.onerror = (event) => {
        const error = (event.target as IDBRequest).error
        console.error('IndexedDB 加载错误:', error)
        reject(new Error(`从 IndexedDB 加载失败: ${error?.message || '未知错误'}`))
      }
      
      transaction.onerror = (event) => {
        const error = (event.target as IDBTransaction).error
        console.error('IndexedDB 事务错误:', error)
        reject(new Error(`IndexedDB 事务失败: ${error?.message || '未知错误'}`))
      }
    })
  } catch (error: any) {
    console.error('IndexedDB 加载异常:', error)
    return null
  }
}

// 检查 localStorage 是否可用且有足够空间
function checkLocalStorageAvailable(): boolean {
  try {
    const testKey = '__localStorage_test__'
    localStorage.setItem(testKey, 'test')
    localStorage.removeItem(testKey)
    return true
  } catch (error) {
    return false
  }
}

// 估算数据大小（字节）
function estimateDataSize(data: any): number {
  try {
    const jsonString = JSON.stringify(data)
    return new Blob([jsonString]).size
  } catch (error) {
    return 0
  }
}

// 保存环境模式
function saveEnvironment() {
  try {
    localStorage.setItem(ENVIRONMENT_STORAGE_KEY, environment.value)
    console.log(`环境模式已保存: ${environment.value}`)
  } catch (error) {
    console.error('保存环境模式失败:', error)
  }
}

// 加载环境模式
function loadEnvironment(): 'testbed' | 'realnet' {
  try {
    const savedEnv = localStorage.getItem(ENVIRONMENT_STORAGE_KEY)
    if (savedEnv === 'testbed' || savedEnv === 'realnet') {
      console.log(`恢复环境模式: ${savedEnv}`)
      return savedEnv
    }
  } catch (error) {
    console.error('加载环境模式失败:', error)
  }
  return 'testbed' // 默认值
}

// 保存状态到localStorage或IndexedDB（根据指定环境，默认为当前环境）
async function saveState(env?: 'testbed' | 'realnet') {
  const state = {
    availableNodes: availableNodes.value,
    canvasNodes: canvasNodes.value,
    connections: connections.value,
    attackTarget: {
      ip: attackTarget.ip,
      x: attackTarget.x,
      y: attackTarget.y
    },
    victimType: victimType.value,
    targetManuallyMoved: targetManuallyMoved.value,
    canvasWidth: canvasWidth.value,
    canvasHeight: canvasHeight.value,
    nextAvailId: nextAvailId,
    nextCanvasId: nextCanvasId,
    isMonitoring: isMonitoring.value,  // 保存攻击状态
    attackTaskId: attackTaskId.value,  // 保存攻击任务ID
    clusterCount: clusterCount.value   // 保存后端聚类数量
  }
  
  const storageKey = getStorageKey(env)
  const envValue = env || environment.value
  
  // 估算数据大小
  const dataSize = estimateDataSize(state)
  const dataSizeMB = (dataSize / (1024 * 1024)).toFixed(2)
  const nodeCount = availableNodes.value.length
  console.log(`准备保存状态 (${envValue}): ${dataSizeMB} MB, ${nodeCount} 个节点`)
  
  // 对于大数据（>= 2MB 或节点数 >= 1000），强制使用 IndexedDB
  const useIndexedDB = dataSize >= 2 * 1024 * 1024 || nodeCount >= 1000
  
  if (useIndexedDB) {
    // 强制使用 IndexedDB（适合大数据）
    console.log(`数据较大，使用 IndexedDB 保存 (${dataSizeMB} MB, ${nodeCount} 个节点)`)
    try {
      await saveToIndexedDB(storageKey, state)
      console.log(`✅ 状态已保存到 IndexedDB (${envValue}): ${dataSizeMB} MB, ${nodeCount} 个节点`)
      
      // 从 localStorage 中删除（如果有的话），避免数据不一致
      try {
        localStorage.removeItem(storageKey)
  } catch (error) {
        // 忽略错误
      }
      return
    } catch (error: any) {
      console.error(`❌ 保存状态到 IndexedDB 失败 (${envValue}):`, error)
      // 尝试降级到 localStorage（虽然可能失败，但至少尝试一下）
      console.warn(`尝试降级到 localStorage...`)
    }
  }
  
  // 尝试使用 localStorage（适合小数据）
  if (checkLocalStorageAvailable()) {
    try {
      const jsonString = JSON.stringify(state)
      localStorage.setItem(storageKey, jsonString)
      console.log(`✅ 状态已保存到 localStorage (${envValue}): ${dataSizeMB} MB, ${nodeCount} 个节点`)
      
      // 更新存储标记
      try {
        const markers = JSON.parse(localStorage.getItem(STORAGE_MARKER_KEY) || '{}')
        markers[storageKey] = 'localstorage'
        localStorage.setItem(STORAGE_MARKER_KEY, JSON.stringify(markers))
      } catch (e) {
        // 忽略标记保存失败
      }
      
      // 同时从 IndexedDB 中删除（如果有的话），因为 localStorage 优先
      try {
        const db = await openIndexedDB()
        const transaction = db.transaction([IDB_STORE_NAME], 'readwrite')
        const store = transaction.objectStore(IDB_STORE_NAME)
        store.delete(storageKey)
      } catch (idbError) {
        // IndexedDB 删除失败不影响，忽略
      }
      return
    } catch (error: any) {
      // localStorage 失败（可能是大小限制），尝试 IndexedDB
      console.warn(`localStorage 保存失败 (${error.message})，尝试使用 IndexedDB`)
      try {
        await saveToIndexedDB(storageKey, state)
        console.log(`✅ 状态已保存到 IndexedDB (降级保存) (${envValue}): ${dataSizeMB} MB`)
        return
      } catch (idbError: any) {
        console.error(`❌ 保存状态完全失败 (${envValue}):`, idbError)
        throw new Error(`无法保存状态: localStorage 和 IndexedDB 都失败了。数据大小: ${dataSizeMB} MB`)
      }
    }
  } else {
    // localStorage 不可用，只能使用 IndexedDB
    try {
      await saveToIndexedDB(storageKey, state)
      console.log(`✅ 状态已保存到 IndexedDB (localStorage 不可用) (${envValue}): ${dataSizeMB} MB`)
    } catch (error: any) {
      console.error(`❌ 保存状态失败 (${envValue}):`, error)
      throw new Error(`无法保存状态: IndexedDB 失败。数据大小: ${dataSizeMB} MB`)
    }
  }
}

// 从localStorage或IndexedDB恢复状态（根据指定环境，默认为当前环境）
async function loadState(env?: 'testbed' | 'realnet') {
  try {
    const envValue = env || environment.value
    const storageKey = getStorageKey(env)
    
    // 无论是否找到保存的状态，都先清空当前状态，避免显示旧环境的数据
    availableNodes.value = []
    canvasNodes.value = []
    connections.value = []
    
    let state: any = null
    
    // 检查存储标记，确定数据存储位置
    let storageLocation: 'localstorage' | 'indexeddb' | 'unknown' = 'unknown'
    try {
      const markers = JSON.parse(localStorage.getItem(STORAGE_MARKER_KEY) || '{}')
      storageLocation = markers[storageKey] || 'unknown'
    } catch (e) {
      // 忽略标记读取失败
    }
    
    // 根据标记优先从对应位置加载，如果没有标记则都尝试
    if (storageLocation === 'localstorage' || storageLocation === 'unknown') {
      try {
        const savedState = localStorage.getItem(storageKey)
        if (savedState) {
          state = JSON.parse(savedState)
          const nodeCount = state.availableNodes?.length || 0
          console.log(`✅ 从 localStorage 加载状态 (${envValue}): ${nodeCount} 个节点`)
        }
      } catch (error: any) {
        console.warn(`从 localStorage 加载失败: ${error.message}`)
      }
    }
    
    // 如果 localStorage 中没有，尝试从 IndexedDB 加载
    if (!state && (storageLocation === 'indexeddb' || storageLocation === 'unknown')) {
      try {
        state = await loadFromIndexedDB(storageKey)
        if (state) {
          const nodeCount = state.availableNodes?.length || 0
          console.log(`✅ 从 IndexedDB 加载状态 (${envValue}): ${nodeCount} 个节点`)
          // 更新存储标记
          try {
            const markers = JSON.parse(localStorage.getItem(STORAGE_MARKER_KEY) || '{}')
            markers[storageKey] = 'indexeddb'
            localStorage.setItem(STORAGE_MARKER_KEY, JSON.stringify(markers))
          } catch (e) {
            // 忽略标记保存失败
          }
        }
      } catch (error: any) {
        console.warn(`从 IndexedDB 加载失败: ${error.message}`)
      }
    }
    
    if (!state) {
      console.log(`未找到 ${envValue} 环境的保存状态，已清空当前状态`)
      // 重置其他状态为默认值
      attackTarget.ip = ''
      attackTarget.x = 0
      attackTarget.y = 0
      targetManuallyMoved.value = false
      attackTaskId.value = ''
      clusterCount.value = null
      return false
    }
    
    // 恢复数据
    const nodeCount = state.availableNodes?.length || 0
    availableNodes.value = state.availableNodes || []
    canvasNodes.value = state.canvasNodes || []
    connections.value = state.connections || []
    
    console.log(`✅ 状态恢复完成 (${envValue}): ${nodeCount} 个节点, ${state.canvasNodes?.length || 0} 个画布节点, ${state.connections?.length || 0} 个连接`)
    
    if (state.attackTarget) {
      attackTarget.ip = state.attackTarget.ip || ''
      attackTarget.x = state.attackTarget.x || 0
      attackTarget.y = state.attackTarget.y || 0
    } else {
      attackTarget.ip = ''
      attackTarget.x = 0
      attackTarget.y = 0
    }
    
    if (state.victimType) {
      victimType.value = state.victimType
    }
    
    if (state.targetManuallyMoved !== undefined) {
      targetManuallyMoved.value = state.targetManuallyMoved
    } else {
      targetManuallyMoved.value = false
    }
    
    if (state.canvasWidth) canvasWidth.value = state.canvasWidth
    if (state.canvasHeight) canvasHeight.value = state.canvasHeight
    if (state.nextAvailId) nextAvailId = state.nextAvailId
    if (state.nextCanvasId) nextCanvasId = state.nextCanvasId
    
    // 恢复攻击任务ID
    if (state.attackTaskId) {
      attackTaskId.value = state.attackTaskId
      console.log('恢复攻击任务ID:', attackTaskId.value)
    } else {
      attackTaskId.value = ''
    }
    
    // 恢复后端聚类数量
    if (state.clusterCount !== null && state.clusterCount !== undefined) {
      clusterCount.value = state.clusterCount
      console.log('恢复后端聚类数量:', clusterCount.value)
    } else {
      clusterCount.value = null
    }
    
    // 注意：不再自动恢复攻击状态，态势感知默认应为未启用
    // 用户需要手动点击"开始攻击"来启动态势感知
    // 这样可以确保每次打开页面时，态势感知都是未启用状态
    
    console.log(`状态已从localStorage恢复 (${envValue})`)
    return true
  } catch (error) {
    console.error('恢复状态失败:', error)
    // 出错时也要清空状态，避免显示错误数据
    availableNodes.value = []
    canvasNodes.value = []
    connections.value = []
    return false
  }
}

// 清空所有状态
// 清空画布（只取消部署，保留节点列表）
function clearCanvas() {
  if (canvasNodes.value.length === 0) {
    alert('画布已经是空的')
    return
  }
  
  if (confirm(`确定要清空画布吗？\n这将取消部署所有 ${canvasNodes.value.length} 个节点，但不会删除节点列表中的节点。`)) {
    canvasNodes.value = []
    connections.value = []
    attackTarget.ip = ''
    targetManuallyMoved.value = false
    updateTargetPosition()
    
    console.log('画布已清空，节点已取消部署')
    alert('画布已清空，所有节点已取消部署')
  }
}

// 清空节点列表（清空所有节点和画布）
function clearAllNodes() {
  const totalNodes = availableNodes.value.length
  const deployedNodes = canvasNodes.value.length
  
  if (totalNodes === 0) {
    alert('节点列表已经是空的')
    return
  }
  
  if (confirm(
    `确定要清空节点列表吗？此操作不可撤销。\n\n` +
    `将删除:\n` +
    `- 节点列表中的所有节点: ${totalNodes} 个\n` +
    `- 画布上已部署的节点: ${deployedNodes} 个\n` +
    `- 所有连接关系\n\n` +
    `提示: 如果只想清空画布，请使用"清空画布"按钮。`
  )) {
    availableNodes.value = []
    canvasNodes.value = []
    connections.value = []
    attackTarget.ip = ''
    targetManuallyMoved.value = false
    canvasWidth.value = 5000
    canvasHeight.value = 5000
    nextAvailId = 1
    nextCanvasId = 1
    updateTargetPosition()
    
    // 清除当前环境的localStorage
    const storageKey = getStorageKey()
    localStorage.removeItem(storageKey)
    console.log('节点列表和画布已全部清空')
    alert('节点列表已清空')
  }
}

// 点击右侧列表添加到画布（自动排布）
// 原有的点击部署功能已删除，改为拖拽部署

// 画布节点拖动
const dragging = reactive<{ nodeId: number | null; offsetX: number; offsetY: number }>({ nodeId: null, offsetX: 0, offsetY: 0 })
function onNodeMouseDown(e: MouseEvent, node: CanvasNode) {
  if (e.button !== 0) return
  const target = e.currentTarget as HTMLElement
  const rect = target.getBoundingClientRect()
  dragging.nodeId = node.id
  
  // 考虑画布的缩放和平移变换
  const canvasRect = canvasRef.value?.getBoundingClientRect()
  if (canvasRect) {
    // 将鼠标坐标转换为画布坐标系
    const mouseX = e.clientX - canvasRect.left
    const mouseY = e.clientY - canvasRect.top
    
    // 反向变换：去除画布的平移和缩放
    const transformedX = (mouseX - translateX.value) / scale.value
    const transformedY = (mouseY - translateY.value) / scale.value
    
    // 计算相对于节点的偏移
    dragging.offsetX = transformedX - node.x
    dragging.offsetY = transformedY - node.y
  } else {
    // 备用方案：使用原始方法
  dragging.offsetX = e.clientX - rect.left
  dragging.offsetY = e.clientY - rect.top
  }
  
  window.addEventListener('mousemove', onWindowMouseMove)
  window.addEventListener('mouseup', onWindowMouseUp, { once: true })
}
function onWindowMouseMove(e: MouseEvent) {
  if (!canvasRef.value || !dragging.nodeId) return
  const rect = canvasRef.value.getBoundingClientRect()
  const node = canvasNodes.value.find(n => n.id === dragging.nodeId)
  if (!node) return
  
  // 将鼠标坐标转换为画布坐标系
  const mouseX = e.clientX - rect.left
  const mouseY = e.clientY - rect.top
  
  // 反向变换：去除画布的平移和缩放
  const transformedX = (mouseX - translateX.value) / scale.value
  const transformedY = (mouseY - translateY.value) / scale.value
  
  // 计算新的节点位置
  const nx = transformedX - dragging.offsetX
  const ny = transformedY - dragging.offsetY
  
  // 移除画布范围限制，允许节点自由移动
  node.x = nx
  node.y = ny
}
function onWindowMouseUp() {
  dragging.nodeId = null
  window.removeEventListener('mousemove', onWindowMouseMove)
}

// 切换后端展开/收起
function toggleBackendExpanded(nodeId: number, event?: MouseEvent) {
  if (event) {
    event.stopPropagation()
  }
  
  const node = canvasNodes.value.find(n => n.id === nodeId)
  if (node) {
    node.backendExpanded = !node.backendExpanded
    console.log(`节点 ${node.name} 后端${node.backendExpanded ? '展开' : '收起'}`)
  }
}

// 根据cluster_group将后端分组
function groupBackendsByCluster(backendConfigs?: BackendConfig[]): BackendGroup[] {
  if (!backendConfigs || backendConfigs.length === 0) {
    return []
  }
  
  // 按cluster_group分组
  const clusterMap = new Map<number, number[]>()
  
  backendConfigs.forEach((config, index) => {
    const clusterGroup = config.cluster_group !== undefined ? config.cluster_group : -1  // 未分组的用-1表示
    if (!clusterMap.has(clusterGroup)) {
      clusterMap.set(clusterGroup, [])
    }
    clusterMap.get(clusterGroup)!.push(index)
  })
  
  // 转换为BackendGroup数组，按cluster_group排序
  const groups: BackendGroup[] = []
  const sortedClusters = Array.from(clusterMap.keys()).sort((a, b) => a - b)
  
  sortedClusters.forEach(clusterGroup => {
    const indices = clusterMap.get(clusterGroup)!
    groups.push({
      backendIndices: indices,
      expanded: false
    })
  })
  
  return groups
}

// 连线相关
const NODE_WIDTH = 180  // 调整宽度以容纳长节点名称（如"节点-DRS-192.168.200.123"）
const NODE_HEADER = 40 // 更新为实际头部高度（min-height: 32px + padding）
const BACKEND_ROW = 26
const NODE_PADDING = 8

function nodeHeight(backends: number) {
  return NODE_HEADER + NODE_PADDING + backends * BACKEND_ROW + NODE_PADDING
}
function portPoint(node: CanvasNode | null, port: Port) {
  if (port.kind === 'target') {
    // 攻击目标的中心点（始终使用SVG图标的中心，而不是容器的中心）
    // SVG图标是容器的第二个子元素（第一个是切换开关），大小为 TARGET_SIZE x TARGET_SIZE
    // 切换开关高度约为 26px (18px开关 + 4px padding * 2) + 4px margin-bottom = 30px
    const switchHeight = 30 // 切换开关的高度（包括margin-bottom）
    const iconY = attackTarget.y + switchHeight // SVG图标的起始y位置
    return { x: attackTarget.x + TARGET_SIZE / 2, y: iconY + TARGET_SIZE / 2 }
  }
  
  if (!node) return { x: 0, y: 0 }
  
  const x = node.x
  const y = node.y
  const nodeHeight = 50 // 节点固定高度
  const portRadius = 8  // 圆点半径（16px宽度的一半）
  
  if (port.kind === 'frontend') {
    // 前端CSS: left: -16px, top: 50%, transform: translateY(-50%)
    // 圆点中心在: node.x + (-16 + 8) = node.x - 8
    // 实际测量偏移: X +2px, Y +14px
    return { x: x - portRadius + 2, y: y + nodeHeight / 2 + 14 }
  } else {
    // 后端连接点
    const idx = port.backendIndex ?? 0
    
    // 如果后端展开，直接返回独立位置
    if (node.backendExpanded) {
    const backendCount = node.backends || 1
    const offsetY = (idx - (backendCount - 1) / 2) * 20
    return { x: x + NODE_WIDTH + portRadius + 18, y: y + nodeHeight / 2 + offsetY + 14 }
    }
    
    // 如果后端未展开
    // 检查是否有后端分组信息（无论是自动编排还是手动拖拽的节点）
    if (node.backendGroups && node.backendGroups.length > 0) {
      // 根据后端分组计算位置（适用于自动编排和手动拖拽的节点）
      // 找到这个后端索引属于哪个分组
      let groupIndex = -1
      for (let i = 0; i < node.backendGroups.length; i++) {
        if (node.backendGroups[i].backendIndices.includes(idx)) {
          groupIndex = i
          break
        }
      }
      
      // 如果找到分组，根据分组索引计算位置
      if (groupIndex >= 0) {
        const groupCount = node.backendGroups.length
        const offsetY = (groupIndex - (groupCount - 1) / 2) * 24
        return { x: x + NODE_WIDTH + portRadius + 18, y: y + nodeHeight / 2 + offsetY + 14 }
      } else {
        // 如果没找到分组（不应该发生），使用中心点
        return { x: x + NODE_WIDTH + portRadius + 18, y: y + nodeHeight / 2 + 14 }
      }
    } else {
      // 没有后端分组信息：所有后端合并到中心点
      return { x: x + NODE_WIDTH + portRadius + 18, y: y + nodeHeight / 2 + 14 }
    }
  }
}

const linkingFrom = ref<Port | null>(null)
const linkingCursor = reactive<{ x: number; y: number }>({ x: 0, y: 0 })
function beginLinkFrom(port: Port, e: MouseEvent) {
  e.stopPropagation()
  // 只响应左键点击（button === 0）
  if (e.button !== 0) return
  // 仅允许从后端端口发起连线
  if (port.kind !== 'backend') return
  linkingFrom.value = port
  // 立即设置初始鼠标位置，避免虚线从旧位置开始
  linkingCursor.x = e.clientX
  linkingCursor.y = e.clientY
  window.addEventListener('mousemove', onLinkMouseMove)
  window.addEventListener('mouseup', cancelLink)
}
function onLinkMouseMove(e: MouseEvent) { linkingCursor.x = e.clientX; linkingCursor.y = e.clientY }
function finishLinkTo(target: Port, e: MouseEvent) {
  e.stopPropagation()
  if (!linkingFrom.value) return
  // 允许连接到"前端"端口或"攻击目标"，且不可连接同一节点
  if (target.kind !== 'frontend' && target.kind !== 'target') return
  if (linkingFrom.value.nodeId === target.nodeId) {
    cancelLink()
    return
  }
  
  // 如果是从后端端口连接，检查是否属于聚类分组
  if (linkingFrom.value.kind === 'backend' && linkingFrom.value.nodeId !== 'target') {
    const fromNode = canvasNodes.value.find(n => n.id === linkingFrom.value!.nodeId)
    if (fromNode && fromNode.backendGroups && fromNode.backendGroups.length > 0) {
      // 找到该后端索引所属的分组
      const backendIndex = linkingFrom.value.backendIndex ?? 0
      const group = fromNode.backendGroups.find(g => g.backendIndices.includes(backendIndex))
      
      if (group && group.backendIndices.length > 1) {
        // 这是一个聚类分组，为该分组中的所有后端创建连接
        group.backendIndices.forEach(idx => {
          // 检查是否已存在相同的连接，避免重复
          const existingConn = connections.value.find(conn => 
            conn.from.nodeId === linkingFrom.value!.nodeId &&
            conn.from.kind === 'backend' &&
            conn.from.backendIndex === idx &&
            conn.to.nodeId === target.nodeId &&
            conn.to.kind === target.kind
          )
          
          if (!existingConn) {
            connections.value.push({
              from: {
                nodeId: linkingFrom.value!.nodeId,
                kind: 'backend',
                backendIndex: idx
              },
              to: target
            })
          }
        })
        cancelLink()
        return
      }
    }
  }
  
  // 普通连接：单个后端到目标
  connections.value.push({ from: linkingFrom.value, to: target })
  cancelLink()
}
function cancelLink() {
  linkingFrom.value = null
  window.removeEventListener('mousemove', onLinkMouseMove)
  window.removeEventListener('mouseup', cancelLink)
}

// 删除端口的所有连接线
function deletePortConnections(port: Port, e: MouseEvent) {
  e.preventDefault()
  e.stopPropagation()
  
  // 如果是从后端端口删除，检查是否属于聚类分组
  let backendIndicesToDelete: number[] | null = null
  let portName = ''
  
  if (port.kind === 'backend' && port.nodeId !== 'target') {
    const node = canvasNodes.value.find(n => n.id === port.nodeId)
    if (node && node.backendGroups && node.backendGroups.length > 0) {
      const backendIndex = port.backendIndex ?? 0
      const group = node.backendGroups.find(g => g.backendIndices.includes(backendIndex))
      
      if (group && group.backendIndices.length > 1) {
        // 这是一个聚类分组，需要删除该分组中所有后端的连接
        backendIndicesToDelete = group.backendIndices
        portName = `后端组（包含 ${group.backendIndices.length} 个后端）`
      }
    }
  }
  
  // 找到所有与该端口相关的连接
  const portMatches = (p1: Port, p2: Port): boolean => {
    return p1.nodeId === p2.nodeId && 
           p1.kind === p2.kind && 
           p1.backendIndex === p2.backendIndex
  }
  
  let connectedLines: typeof connections.value = []
  
  if (backendIndicesToDelete && backendIndicesToDelete.length > 1) {
    // 删除聚类分组中所有后端的连接
    connectedLines = connections.value.filter(conn => {
      if (conn.from.nodeId === port.nodeId && conn.from.kind === 'backend') {
        return backendIndicesToDelete!.includes(conn.from.backendIndex ?? 0)
      }
      if (conn.to.nodeId === port.nodeId && conn.to.kind === 'backend') {
        return backendIndicesToDelete!.includes(conn.to.backendIndex ?? 0)
      }
      return false
    })
  } else {
    // 普通删除：单个端口
    connectedLines = connections.value.filter(conn => 
    portMatches(conn.from, port) || portMatches(conn.to, port)
  )
  }
  
  if (connectedLines.length === 0) {
    return // 没有连接线，不显示确认框
  }
  
  if (!portName) {
    portName = port.kind === 'target' 
    ? '攻击目标' 
    : port.kind === 'frontend' 
      ? '前端' 
      : `后端${(port.backendIndex ?? 0) + 1}`
  }
  
  if (confirm(`确定要删除该${portName}的 ${connectedLines.length} 条连接线吗？`)) {
    if (backendIndicesToDelete && backendIndicesToDelete.length > 1) {
      // 删除聚类分组中所有后端的连接
      connections.value = connections.value.filter(conn => {
        if (conn.from.nodeId === port.nodeId && conn.from.kind === 'backend') {
          return !backendIndicesToDelete!.includes(conn.from.backendIndex ?? 0)
        }
        if (conn.to.nodeId === port.nodeId && conn.to.kind === 'backend') {
          return !backendIndicesToDelete!.includes(conn.to.backendIndex ?? 0)
        }
        return true
      })
    } else {
      // 普通删除：单个端口
    connections.value = connections.value.filter(conn => 
      !portMatches(conn.from, port) && !portMatches(conn.to, port)
    )
    }
  }
}

const tempLine = computed(() => {
  if (!linkingFrom.value || !canvasRef.value) return null
  const fromNode = linkingFrom.value.nodeId === 'target' ? null : canvasNodes.value.find(n => n.id === linkingFrom.value!.nodeId) || null
  const { x: fx, y: fy } = portPoint(fromNode, linkingFrom.value)
  
  // 获取 canvas-content 元素的 bounding rect（已经应用了 transform）
  const canvasContent = canvasRef.value.querySelector('.canvas-content') as HTMLElement
  if (!canvasContent) return null
  
  const contentRect = canvasContent.getBoundingClientRect()
  
  // 屏幕坐标 -> canvas-content 内部坐标
  // 因为 SVG 在 canvas-content 内部，它会自动受到 transform 影响
  // 所以我们需要将屏幕坐标转换为 canvas-content 内部的坐标
  const canvasX = (linkingCursor.x - contentRect.left) / scale.value
  const canvasY = (linkingCursor.y - contentRect.top) / scale.value
  
  return { x1: fx, y1: fy, x2: canvasX, y2: canvasY }
})

// 右键取消部署节点（从画布移除，但不删除节点本身）
function onNodeContextMenu(e: MouseEvent, node: CanvasNode) {
  e.preventDefault()
  const id = node.id
  
  // 从画布移除节点（取消部署）
  const idx = canvasNodes.value.findIndex(n => n.id === id)
  if (idx >= 0) {
    canvasNodes.value.splice(idx, 1)
    
    // 移除相关连接
  connections.value = connections.value.filter(c => c.from.nodeId !== id && c.to.nodeId !== id)
    
    // 注意：节点仍保留在 availableNodes 中，只是从画布取消部署
    // 节点会在节点列表中显示为"未部署"状态，可以重新部署
  }
}

// 缩放功能
function handleWheel(e: WheelEvent) {
  e.preventDefault()
  const delta = e.deltaY > 0 ? 0.9 : 1.1
  const newScale = Math.max(0.1, Math.min(5, scale.value * delta))
  
  // 计算鼠标位置相对于画布的坐标
  const rect = canvasRef.value?.getBoundingClientRect()
  if (rect) {
    const mouseX = e.clientX - rect.left
    const mouseY = e.clientY - rect.top
    
    // 计算缩放前后的坐标差异
    const scaleDiff = newScale - scale.value
    const offsetX = mouseX * scaleDiff
    const offsetY = mouseY * scaleDiff
    
    // 调整平移以保持鼠标位置不变
    translateX.value -= offsetX
    translateY.value -= offsetY
  }
  
  scale.value = newScale
}

// 计算变换样式
const transformStyle = computed(() => {
  return `translate(${translateX.value}px, ${translateY.value}px) scale(${scale.value})`
})

// 画布拖拽功能
function handleMouseDown(e: MouseEvent) {
  // 只在空白区域（不是节点或按钮）开始拖拽
  if (e.target === canvasRef.value || (e.target as HTMLElement).classList.contains('canvas-content')) {
    e.preventDefault()
    isDragging.value = true
    dragStart.value = { x: e.clientX, y: e.clientY }
    lastTranslate.value = { x: translateX.value, y: translateY.value }
    
    // 添加全局鼠标事件监听
    document.addEventListener('mousemove', handleMouseMove)
    document.addEventListener('mouseup', handleMouseUp)
    
    // 改变鼠标样式
    if (canvasRef.value) {
      canvasRef.value.style.cursor = 'grabbing'
    }
  }
}

function handleMouseMove(e: MouseEvent) {
  if (!isDragging.value) return
  
  e.preventDefault()
  const deltaX = e.clientX - dragStart.value.x
  const deltaY = e.clientY - dragStart.value.y
  
  translateX.value = lastTranslate.value.x + deltaX
  translateY.value = lastTranslate.value.y + deltaY
}

function handleMouseUp(_e: MouseEvent) {
  if (!isDragging.value) return
  
  isDragging.value = false
  
  // 移除全局鼠标事件监听
  document.removeEventListener('mousemove', handleMouseMove)
  document.removeEventListener('mouseup', handleMouseUp)
  
  // 恢复鼠标样式
  if (canvasRef.value) {
    canvasRef.value.style.cursor = 'grab'
  }
}

// 自动保存机制已移除，改为只在离开页面和切换模式时保存
// 这样可以避免拖拽节点时的卡顿问题
// 状态保存现在在以下时机进行：
// 1. 组件卸载时（onUnmounted）- 离开页面时
// 2. 环境切换时（watch environment）- 切换测试床/实网模式时
// 3. 其他特定操作时（如加载节点、启动/停止态势感知等）

// 初始化标志，用于在组件挂载时跳过watch的保存逻辑
const isInitializing = ref(true)

// 监听环境切换，保存当前环境状态并加载新环境状态
watch(environment, (newEnv, oldEnv) => {
  // 如果正在初始化，跳过保存旧环境状态的逻辑（避免覆盖已保存的状态）
  if (isInitializing.value) {
    return
  }
  
  if (oldEnv !== undefined) {
    console.log(`环境切换：从 ${oldEnv} 切换到 ${newEnv}`)
    
    // 先保存旧环境的状态（使用 oldEnv 参数，避免修改 environment.value）
    saveState(oldEnv).catch(err => console.error('保存状态失败:', err))
    
    // 保存环境模式
    saveEnvironment()
    
    // 加载新环境的状态
    loadState(newEnv).catch(err => console.error('加载状态失败:', err))
    
    // 更新攻击目标位置（仅在未手动移动过时）
    if (!targetManuallyMoved.value) {
      nextTick(() => {
        updateTargetPosition()
      })
    }
  }
})

// 点击外部关闭排序菜单
function handleClickOutside(event: MouseEvent) {
  if (sortButtonWrapper.value && !sortButtonWrapper.value.contains(event.target as Node)) {
    showSortMenu.value = false
  }
}

// 组件挂载时恢复状态
onMounted(async () => {
  // 注册全局搜索接口，供系统级搜索使用
  ;(window as any).topologyDesignerSearch = searchNodesInList
  // 先恢复环境模式
  const savedEnv = loadEnvironment()
  
  // 设置初始化标志，避免watch触发时保存空状态
  isInitializing.value = true
  
  // 设置环境模式（此时watch会被触发，但由于isInitializing为true，不会保存状态）
  environment.value = savedEnv
  
  // 然后加载对应环境的状态
  await loadState()
  
  // 等待本轮响应式更新 & DOM 更新完成，再解除初始化标志
  await nextTick()
  
  // 如果攻击目标位置已保存且不是默认值，且未被手动移动过，保持保存的位置
  // 否则才自动更新位置
  if (targetManuallyMoved.value || (attackTarget.x === 0 && attackTarget.y === 0)) {
    updateTargetPosition()
  } else {
    // 位置已保存，保持原位置，但需要确保位置是有效的
    console.log(`保持攻击目标保存的位置: x=${attackTarget.x}, y=${attackTarget.y}`)
  }
  
  isInitializing.value = false
  
  // 加载配置文件
  await loadConfiguration()
  
  // 添加全局点击监听器，用于关闭排序菜单
  document.addEventListener('click', handleClickOutside)
  
  // 如果画布上有节点，自动进行一次整理画布
  await nextTick()  // 等待DOM更新完成
  if (canvasNodes.value.length > 0) {
    console.log(`检测到画布上有 ${canvasNodes.value.length} 个节点，自动整理画布...`)
    // 延迟一小段时间，确保画布元素已完全渲染
    setTimeout(async () => {
      await resetCanvasView()
      console.log('自动整理画布完成')
    }, 100)
  }
})

// 组件卸载时清理定时器和SSE连接
onUnmounted(() => {
  // 清理全局搜索接口
  if ((window as any).topologyDesignerSearch) {
    delete (window as any).topologyDesignerSearch
  }
  
  // 保存当前状态（包括攻击状态）
  // 注意：onUnmounted 中不能使用 await，所以使用同步方式保存
  // 但由于 saveState 是异步的，这里使用 Promise 但不等待结果
  saveState().catch(err => console.error('卸载时保存状态失败:', err))
  
  // 保存环境模式
  saveEnvironment()
  
  // 清理态势感知定时器（组件卸载后定时器无法正常工作）
  // 但保持isMonitoring状态，以便重新挂载时恢复
  if (monitoringTimer.value !== null) {
    clearInterval(monitoringTimer.value)
    monitoringTimer.value = null
  }
  // 注意：不重置isMonitoring.value，保持攻击状态标记
  
  // 移除全局点击监听器
  document.removeEventListener('click', handleClickOutside)
})

// 加载配置文件
async function loadConfiguration() {
  try {
    const config = await configService.getConfig()
    targetServerInfo.value = `${config.attackChain.protocol}://${config.attackChain.targetHost}:${config.attackChain.targetPort}`
    configLoaded.value = true
    console.log('配置加载成功:', config)
  } catch (error) {
    console.error('配置加载失败:', error)
    targetServerInfo.value = '配置加载失败'
  }
}

// 使用现有节点构建拓扑结构（用于自动编排）
async function buildTopologyFromExistingNodes(tripleStructures: TripleStructure[], targetIP: string | null = null) {
  // 辅助函数：将字符串或数组转换为数组
  const toArray = (value: string | string[]): string[] => {
    return Array.isArray(value) ? value : [value]
  }
  
  // 收集所有涉及的前端IP（排除攻击目标IP）
  const involvedFrontendIps = new Set<string>()
  tripleStructures.forEach(triple => {
    involvedFrontendIps.add(triple.parent_drs_in_ip)
    const childIps = toArray(triple.child_drs_in_ips)
    childIps.forEach(ip => {
      // 如果是攻击目标IP，不添加到前端IP集合中
      if (ip !== targetIP) {
        involvedFrontendIps.add(ip)
      }
    })
  })
  
  console.log(`链结构涉及 ${involvedFrontendIps.size} 个前端IP`)
  
  // 创建IP到节点的映射
  const ipToAvailableNode = new Map<string, AvailableNode>()
  availableNodes.value.forEach(node => {
    if (node.frontendIp) {
      ipToAvailableNode.set(node.frontendIp, node)
    }
  })
  
  console.log(`节点列表中有 ${ipToAvailableNode.size} 个节点`)
  
  // 找到需要部署的节点（基于涉及的前端IP）
  const nodesToDeploy = new Set<AvailableNode>()
  involvedFrontendIps.forEach(ip => {
    const node = ipToAvailableNode.get(ip)
    if (node) {
      nodesToDeploy.add(node)
    }
  })
  
  console.log(`将部署 ${nodesToDeploy.size} 个节点到画布`)
  
  // 创建前端IP到画布节点的映射
  const frontendIpToCanvasNode = new Map<string, CanvasNode>()
  let canvasNodeId = nextCanvasId
  
  // 第一步：将节点部署到画布，并根据聚类信息创建后端分组
  nodesToDeploy.forEach(availableNode => {
    // 根据聚类信息创建后端分组
    const backendGroups = groupBackendsByCluster(availableNode.backendConfigs)
    
    const canvasNode: CanvasNode = {
      id: canvasNodeId++,
      name: availableNode.name,
      backends: availableNode.backends,
      x: 0,
      y: 0,
      frontendIp: availableNode.frontendIp,
      backendIps: availableNode.backendIps,
      level: availableNode.level || 1,
      sourceAvailableNodeId: availableNode.id,
      isAutoArranged: true,  // 标记为自动编排生成
      backendExpanded: false,  // 默认不展开
      backendGroups: backendGroups.length > 0 ? backendGroups : [],  // 根据聚类信息创建分组
      // 复制服务器节点的额外参数
      rd_senstive_flage: availableNode.rd_senstive_flage,
      negative_cache: availableNode.negative_cache,
      frontend_query_merge_time_window: availableNode.frontend_query_merge_time_window,
      backendConfigs: availableNode.backendConfigs,
      // 复制统计字段
      overall_auth_duration: availableNode.overall_auth_duration,
      overall_vic_duration: availableNode.overall_vic_duration,
      total_request_auth_times: availableNode.total_request_auth_times,
      total_request_vic_times: availableNode.total_request_vic_times
    }
    
    if (availableNode.frontendIp) {
      frontendIpToCanvasNode.set(availableNode.frontendIp, canvasNode)
    }
    canvasNodes.value.push(canvasNode)
  })
  
  // 更新 nextCanvasId
  nextCanvasId = canvasNodeId
  
  console.log(`已部署 ${canvasNodes.value.length} 个节点到画布`)
  
  // 第二步：根据三元组创建连接（后端分组已根据聚类信息创建，这里不再修改分组）
  tripleStructures.forEach(triple => {
    const frontendIp = triple.parent_drs_in_ip
    const backendIps = toArray(triple.parent_drs_backend_ip)
    const childIps = toArray(triple.child_drs_in_ips)
    
    const parentCanvasNode = frontendIpToCanvasNode.get(frontendIp)
    if (!parentCanvasNode || !parentCanvasNode.backendIps) return
    
    // 注意：这里不再创建新的后端组，因为分组已经根据聚类信息创建好了
    // 只需要根据三元组创建连接即可
    childIps.forEach(childIp => {
      // 找到这些后端IP在节点中的索引
      const backendIndices: number[] = []
      backendIps.forEach(backendIp => {
        const idx = parentCanvasNode.backendIps!.indexOf(backendIp)
        if (idx >= 0) {
          backendIndices.push(idx)
        }
      })
      
      if (backendIndices.length === 0) return
      
      // 确定目标节点ID（使用canvasNode的ID）
      let targetNodeId: number | 'target' | undefined
      if (childIp === targetIP) {
        targetNodeId = 'target'
      } else {
        const childCanvasNode = frontendIpToCanvasNode.get(childIp)
        targetNodeId = childCanvasNode ? childCanvasNode.id : undefined
      }
      
      if (!targetNodeId) return
      
      // 为每个后端索引创建连接
      backendIndices.forEach(backendIndex => {
        if (childIp === targetIP) {
          // 连接到攻击目标
          connections.value.push({
            from: {
              nodeId: parentCanvasNode.id,
              kind: 'backend',
              backendIndex: backendIndex
            },
            to: {
              nodeId: 'target',
              kind: 'target'
            }
          })
        } else {
          // 连接到普通节点
          connections.value.push({
            from: {
              nodeId: parentCanvasNode.id,
              kind: 'backend',
              backendIndex: backendIndex
            },
            to: {
              nodeId: targetNodeId as number,
              kind: 'frontend'
            }
          })
        }
      })
      
      // 注意：后端分组已经根据聚类信息创建好了，这里不再修改分组
      // 只需要根据三元组创建连接即可
    })
  })
  
  // 强制触发Vue的响应式更新
  canvasNodes.value = [...canvasNodes.value]
  
  // 分析层级关系
  const levelMap = new Map<string, number>()
  const processedNodes = new Set<string>()
  
  // 找到根节点（只作为parent_drs_in_ip出现，不作为child_drs_in_ips出现）
  const parentIps = new Set(tripleStructures.map(t => t.parent_drs_in_ip))
  const allChildIps = new Set<string>()
  tripleStructures.forEach(t => {
    const childIps = toArray(t.child_drs_in_ips)
    childIps.forEach(ip => allChildIps.add(ip))
  })
  const rootIps = Array.from(parentIps).filter(ip => !allChildIps.has(ip))
  
  // 从根节点开始分配层级
  function assignLevels(ip: string, level: number) {
    // 跳过攻击目标IP
    if (ip === targetIP) return
    if (levelMap.has(ip)) return
    levelMap.set(ip, level)
    processedNodes.add(ip)
    
    // 找到该IP作为parent的所有子节点
    const childTriples = tripleStructures.filter(t => t.parent_drs_in_ip === ip)
    childTriples.forEach(triple => {
      const childIps = toArray(triple.child_drs_in_ips)
      childIps.forEach(childIp => {
        // 跳过攻击目标IP
        if (childIp !== targetIP) {
        assignLevels(childIp, level + 1)
        }
      })
    })
  }
  
  // 为所有根节点分配层级
  rootIps.forEach(rootIp => {
    // 跳过攻击目标IP
    if (rootIp !== targetIP) {
      assignLevels(rootIp, 1)
    }
  })
  
  // 处理剩余的节点（处理循环引用的情况）
  involvedFrontendIps.forEach(ip => {
    if (!processedNodes.has(ip) && ip !== targetIP) {
      assignLevels(ip, 1)
    }
  })
  
  // 更新节点的层级信息
  canvasNodes.value.forEach(node => {
    if (node.frontendIp) {
      node.level = levelMap.get(node.frontendIp) || 1
    }
  })
  
  // 重建连接：基于三元组创建连接（支持新格式的后端分组）
  tripleStructures.forEach(triple => {
    const parentNode = frontendIpToCanvasNode.get(triple.parent_drs_in_ip)
    if (!parentNode) return
    
    const backendIps = toArray(triple.parent_drs_backend_ip)
    const childIps = toArray(triple.child_drs_in_ips)
    
    // 为每个后端IP和子节点IP的组合创建连接
    backendIps.forEach(backendIp => {
      const backendIndex = parentNode.backendIps?.indexOf(backendIp) ?? 0
      
      childIps.forEach(childIp => {
        if (childIp === targetIP) {
          // 如果子节点IP是攻击目标，创建连接到target的连接
          connections.value.push({
            from: {
              nodeId: parentNode.id,
              kind: 'backend',
              backendIndex: backendIndex
            },
            to: {
              nodeId: 'target',
              kind: 'target'
            }
          })
          console.log(`创建连接到攻击目标: ${parentNode.name}[后端${backendIndex}] -> target (${targetIP})`)
        } else {
          // 否则连接到普通节点
        const childNode = frontendIpToCanvasNode.get(childIp)
        if (childNode) {
          connections.value.push({
            from: {
              nodeId: parentNode.id,
              kind: 'backend',
              backendIndex: backendIndex
            },
            to: {
              nodeId: childNode.id,
              kind: 'frontend'
            }
          })
        }
    }
      })
    })
  })
  
  console.log(`创建了 ${connections.value.length} 个连接`)
  
  // 等待Vue更新DOM
  await nextTick()
  
  // 自动布局：轴对称树形布局
  await performAutoLayout()
  
  // 自动调整画布大小以适应所有节点
  await adjustCanvasSize()
}

// 自动布局算法：对称树形布局，支持多个根节点
async function performAutoLayout() {
  if (canvasNodes.value.length === 0) return
  
  // 识别根节点（前端没有被连接的节点）
  const connectedFrontendNodeIds = new Set<number>()
  connections.value.forEach(conn => {
    if (conn.to.kind === 'frontend' && conn.to.nodeId !== 'target') {
      connectedFrontendNodeIds.add(conn.to.nodeId as number)
    }
  })
  
  const rootNodes = canvasNodes.value.filter(node => 
    !connectedFrontendNodeIds.has(node.id)
  )
  
  console.log(`布局时识别到 ${rootNodes.length} 个根节点`)
  
  // 按层级分组节点
  const nodesByLevel = new Map<number, CanvasNode[]>()
  canvasNodes.value.forEach(node => {
    const level = node.level || 1
    if (!nodesByLevel.has(level)) {
      nodesByLevel.set(level, [])
    }
    nodesByLevel.get(level)!.push(node)
  })
  
  // 布局参数
  const levelWidth = 400   // 层级间距（水平方向）
  const nodeSpacing = 120  // 同层节点间距（垂直方向）
  const startX = 150       // 起始X坐标
  const chainSpacing = 300 // 多条攻击链之间的间距（垂直方向）
  
  const sortedLevels = Array.from(nodesByLevel.keys()).sort((a, b) => a - b)
  
  // 如果只有一个根节点，使用原来的布局方式
  if (rootNodes.length <= 1) {
    // 第一遍：计算每层的总高度
    const levelHeights = new Map<number, number>()
    
    sortedLevels.forEach(level => {
      const nodes = nodesByLevel.get(level) || []
      const nodeCount = nodes.length
      const totalHeight = Math.max(0, (nodeCount - 1) * nodeSpacing)
      levelHeights.set(level, totalHeight)
    })
    
    // 找出最大高度，作为参考中心
    const maxHeight = Math.max(...Array.from(levelHeights.values()))
    const centerY = 200 + maxHeight / 2  // 画布中心Y坐标
    
    // 第二遍：对称布局 - 每层独立居中对齐
  sortedLevels.forEach(level => {
    const nodes = nodesByLevel.get(level) || []
    const nodeCount = nodes.length
    
      if (nodeCount === 0) return
      
      // 计算该层的X坐标
      const x = startX + (level - 1) * levelWidth
      
      // 计算该层的起始Y坐标（居中对齐）
      const totalHeight = (nodeCount - 1) * nodeSpacing
      const startY = centerY - totalHeight / 2
      
      // 为每个节点分配位置
    nodes.forEach((node, index) => {
        node.x = x
        node.y = startY + index * nodeSpacing
      })
    })
  } else {
    // 多个根节点：统一按层级布局，每层所有节点对称排列
    // 这样可以避免共享节点被重复设置位置导致堆叠的问题
    
    // 计算所有层级中节点数最多的层，用于确定整体高度
    let maxNodesInAnyLevel = 0
    sortedLevels.forEach(level => {
      const nodes = nodesByLevel.get(level) || []
      maxNodesInAnyLevel = Math.max(maxNodesInAnyLevel, nodes.length)
    })
    
    // 计算整体布局的中心Y坐标
    const totalHeight = (maxNodesInAnyLevel - 1) * nodeSpacing
    const centerY = 200 + totalHeight / 2
    
    // 按层级统一布局：每层的所有节点都放在一起，对称排列
    sortedLevels.forEach(level => {
      const nodes = nodesByLevel.get(level) || []
      const nodeCount = nodes.length
      
      if (nodeCount === 0) return
      
      // 计算该层的X坐标
      const x = startX + (level - 1) * levelWidth
      
      // 计算该层的起始Y坐标（居中对齐）
      const levelTotalHeight = (nodeCount - 1) * nodeSpacing
      const levelStartY = centerY - levelTotalHeight / 2
      
      // 为每个节点分配位置（按节点ID排序，确保布局稳定）
      const sortedNodes = [...nodes].sort((a, b) => a.id - b.id)
      sortedNodes.forEach((node, index) => {
        node.x = x
        node.y = levelStartY + index * nodeSpacing
    })
  })
}

  // 第四遍：自动调整攻击目标位置
  // 找到最后一层的所有节点（叶子节点）
  if (sortedLevels.length > 0) {
    const lastLevel = sortedLevels[sortedLevels.length - 1]
    const lastLevelNodes = nodesByLevel.get(lastLevel) || []
    
    // 找到有连接到攻击目标的叶子节点
    const leafNodesConnectedToTarget = lastLevelNodes.filter(node => 
      connections.value.some(conn => 
        conn.from.nodeId === node.id && conn.to.nodeId === 'target'
      )
    )
    
    // 如果有连接到攻击目标的节点，将攻击目标移动到它们右侧居中
    if (leafNodesConnectedToTarget.length > 0) {
      // 计算叶子节点的Y坐标范围
      const leafYCoords = leafNodesConnectedToTarget.map(n => n.y)
      const minLeafY = Math.min(...leafYCoords)
      const maxLeafY = Math.max(...leafYCoords)
      const centerLeafY = (minLeafY + maxLeafY) / 2
      
      // 找到最右侧的节点X坐标
      const maxLeafX = Math.max(...leafNodesConnectedToTarget.map(n => n.x))
      
      // 将攻击目标放置在叶子节点右侧
      const targetDistance = 350  // 攻击目标距离叶子节点的距离
      attackTarget.x = maxLeafX + targetDistance
      attackTarget.y = centerLeafY - TARGET_SIZE / 2  // 居中对齐
      
      console.log(`自动调整攻击目标位置: x=${attackTarget.x}, y=${attackTarget.y}（基于${leafNodesConnectedToTarget.length}个叶子节点）`)
    } else if (lastLevelNodes.length > 0) {
      // 如果没有连接到攻击目标，但有最后一层节点，也调整位置
      const lastLevelYCoords = lastLevelNodes.map(n => n.y)
      const centerLastY = (Math.min(...lastLevelYCoords) + Math.max(...lastLevelYCoords)) / 2
      const maxLastX = Math.max(...lastLevelNodes.map(n => n.x))
      
      attackTarget.x = maxLastX + 350
      attackTarget.y = centerLastY - TARGET_SIZE / 2
      
      console.log(`自动调整攻击目标位置（基于最后一层）: x=${attackTarget.x}, y=${attackTarget.y}`)
    }
  }
  
  console.log('自动布局完成')
}

// 自动调整画布大小以适应所有节点
async function adjustCanvasSize() {
  if (canvasNodes.value.length === 0) return
  
  // 计算所有节点的边界
  const nodeSize = 60 // 节点大小（半径）
  const margin = 150  // 边距 - 从100增加到150，留出更多空间
  
  // 收集所有需要考虑的坐标点（包括节点和攻击目标）
  const allXCoords = canvasNodes.value.map(node => node.x)
  const allYCoords = canvasNodes.value.map(node => node.y)
  
  // 如果有攻击目标，也考虑攻击目标的位置
  if (attackTarget.x > 0 || attackTarget.y > 0) {
    allXCoords.push(attackTarget.x + TARGET_SIZE)
    allYCoords.push(attackTarget.y + TARGET_SIZE)
  }
  
  const minX = Math.min(...allXCoords) - nodeSize - margin
  const maxX = Math.max(...allXCoords) + nodeSize + margin
  const minY = Math.min(...allYCoords) - nodeSize - margin
  const maxY = Math.max(...allYCoords) + nodeSize + margin
  
  // 计算需要的画布尺寸
  const requiredWidth = Math.max(1200, maxX - minX + 300)  // 最小宽度从800增加到1200
  const requiredHeight = Math.max(800, maxY - minY + 300)  // 最小高度从520增加到800
  
  // 调整画布大小
  canvasWidth.value = Math.max(canvasWidth.value, requiredWidth)
  canvasHeight.value = Math.max(canvasHeight.value, requiredHeight)
    
    console.log(`画布已自动调整为: ${canvasWidth.value} x ${canvasHeight.value}`)
  }

// 合并所有节点（可用节点 + 画布节点）
// 虚拟滚动相关状态
const nodeListScrollTop = ref(0)
const nodeListContainer = ref<HTMLElement | null>(null)
const NODE_ITEM_HEIGHT = 80 // 每个节点项的大概高度（包括间距）
const VISIBLE_BUFFER = 5 // 可见区域上下缓冲的节点数量


// 优化：缓存部署状态映射，避免每次都重新计算
const deployedNodeIds = computed(() => {
  const deployedSet = new Set<number>()
  canvasNodes.value.forEach(canvasNode => {
    if (canvasNode.sourceAvailableNodeId) {
      deployedSet.add(canvasNode.sourceAvailableNodeId)
    }
  })
  return deployedSet
})

const allNodes = computed(() => {
  // 创建所有可用节点的副本，并计算部署状态（使用缓存的部署状态）
  const nodes = availableNodes.value.map(node => ({
    ...node,
    isDeployed: deployedNodeIds.value.has(node.id)
  }))
  
  // 添加画布上没有对应 availableNode 的节点（旧数据或直接添加的节点）
  canvasNodes.value.forEach(canvasNode => {
    if (!canvasNode.sourceAvailableNodeId) {
      // 没有源节点ID，检查是否已经在nodes中
      const existingNode = nodes.find(n => 
        n.frontendIp === canvasNode.frontendIp && 
        n.name === canvasNode.name
      )
      if (!existingNode) {
        // 添加到节点列表
        nodes.push({
          id: canvasNode.id + 10000, // 避免ID冲突
          name: canvasNode.name,
          backends: canvasNode.backends,
          frontendIp: canvasNode.frontendIp,
          backendIps: canvasNode.backendIps,
          level: canvasNode.level,
          isDeployed: true
        })
      }
    }
  })
  
  return nodes
})


// 虚拟滚动：计算可见的节点范围
const visibleNodes = computed(() => {
  const totalNodes = allNodes.value.length
  
  // 如果节点数量较少（小于100），直接返回所有节点
  if (totalNodes < 100) {
    return {
      nodes: allNodes.value,
      startIndex: 0,
      endIndex: totalNodes - 1,
      offsetY: 0
    }
  }
  
  // 计算可见区域的节点范围
  const containerHeight = nodeListContainer.value?.clientHeight || 600
  let startIndex = Math.max(0, Math.floor(nodeListScrollTop.value / NODE_ITEM_HEIGHT) - VISIBLE_BUFFER)
  const visibleCount = Math.ceil(containerHeight / NODE_ITEM_HEIGHT) + VISIBLE_BUFFER * 2
  let endIndex = Math.min(totalNodes - 1, startIndex + visibleCount)
  
  
  // 返回可见的节点和偏移量
  return {
    nodes: allNodes.value.slice(startIndex, endIndex + 1),
    startIndex,
    endIndex,
    offsetY: startIndex * NODE_ITEM_HEIGHT
  }
})

// 节点列表滚动事件处理
function onNodeListScroll(e: Event) {
  const target = e.target as HTMLElement
  nodeListScrollTop.value = target.scrollTop
}

// 系统级搜索：搜索节点列表中的所有节点（包括未渲染的）
// 存储匹配的节点索引，供导航使用
let nodeSearchMatches: number[] = []
let nodeSearchCurrentIndex = 0

function searchNodesInList(query: string): { 
  found: boolean
  matchCount: number
  scrollToFirst: () => void
  scrollToNext: () => void
  scrollToPrev: () => void
  scrollToIndex: (index: number) => void
} {
  const normalizedQuery = query.trim().toLowerCase()
  
  if (!normalizedQuery) {
    nodeSearchMatches = []
    nodeSearchCurrentIndex = 0
    return { 
      found: false, 
      matchCount: 0, 
      scrollToFirst: () => {},
      scrollToNext: () => {},
      scrollToPrev: () => {},
      scrollToIndex: () => {}
    }
  }
  
  // 在所有节点中搜索（不限于可见节点）
  const matches: number[] = []
  const totalNodes = allNodes.value.length
  
  for (let index = 0; index < totalNodes; index++) {
    const node = allNodes.value[index]
    if (!node) continue
    
    const searchText = [
      node.name || '',
      node.frontendIp || '',
      ...(node.backendIps || [])
    ].filter(Boolean).join(' ').toLowerCase()
    
    if (searchText.includes(normalizedQuery)) {
      matches.push(index)
    }
  }
  
  nodeSearchMatches = matches
  nodeSearchCurrentIndex = 0
  
  const scrollToNodeIndex = (index: number) => {
    if (index < 0 || index >= matches.length || !nodeListContainer.value) return
    
    const targetIndex = matches[index]
    const targetScrollTop = targetIndex * NODE_ITEM_HEIGHT
    
    // 立即更新滚动位置（用于虚拟滚动计算），确保节点会被渲染
    nodeListScrollTop.value = targetScrollTop
    nodeListContainer.value.scrollTop = targetScrollTop
    
    // 等待节点渲染后，再次确保滚动位置正确，并触发 DOM 搜索来高亮文本
    nextTick(() => {
      if (nodeListContainer.value) {
        nodeListScrollTop.value = nodeListContainer.value.scrollTop
        // 等待一小段时间让节点渲染完成，然后触发 DOM 搜索来高亮文本
        setTimeout(() => {
          // 触发自定义事件，通知 AppLayout 重新执行 DOM 搜索来高亮新渲染的节点
          const event = new CustomEvent('node-search-scrolled', {
            detail: { query: normalizedQuery }
          })
          window.dispatchEvent(event)
        }, 150)
      }
    })
  }
  
  const scrollToFirst = () => {
    if (matches.length > 0) {
      nodeSearchCurrentIndex = 0
      scrollToNodeIndex(0)
    }
  }
  
  const scrollToNext = () => {
    if (matches.length > 0) {
      nodeSearchCurrentIndex = (nodeSearchCurrentIndex + 1) % matches.length
      scrollToNodeIndex(nodeSearchCurrentIndex)
    }
  }
  
  const scrollToPrev = () => {
    if (matches.length > 0) {
      nodeSearchCurrentIndex = nodeSearchCurrentIndex <= 0 
        ? matches.length - 1 
        : nodeSearchCurrentIndex - 1
      scrollToNodeIndex(nodeSearchCurrentIndex)
    }
  }
  
  return {
    found: matches.length > 0,
    matchCount: matches.length,
    scrollToFirst,
    scrollToNext,
    scrollToPrev,
    scrollToIndex: scrollToNodeIndex
  }
}

// 获取画布节点在节点列表中的序号
function getNodeIndex(canvasNode: CanvasNode): number | null {
  if (canvasNode.sourceAvailableNodeId) {
    // 如果有源节点ID，在availableNodes中查找
    const index = availableNodes.value.findIndex(n => n.id === canvasNode.sourceAvailableNodeId)
    return index >= 0 ? index + 1 : null
  }
  // 如果没有源节点ID，在allNodes中查找
  const index = allNodes.value.findIndex(n => 
    n.frontendIp === canvasNode.frontendIp && n.name === canvasNode.name
  )
  return index >= 0 ? index + 1 : null
}


// 生成攻击链
async function generateAttackChain() {
  // 检查是否有节点和连接
  if (canvasNodes.value.length === 0) {
    alert('请先添加节点到画布')
    return
  }
  
  if (connections.value.length === 0) {
    alert('请先建立节点之间的连接')
    return
  }
  
  // 检查是否设置了攻击目标IP
  if (!attackTarget.ip.trim()) {
    alert('请设置攻击目标IP地址')
    return
  }
  
  // 检查配置是否已加载
  if (!configLoaded.value) {
    alert('配置尚未加载完成，请稍后再试')
    return
  }
  
  try {
    sendingStatus.value = 'sending'
    
    // 构建攻击链数据
    const attackData: AttackChainData = {
      nodes: canvasNodes.value.map(node => ({
        id: node.id,
        name: node.name,
        backends: node.backends,
        frontendIp: node.frontendIp,
        backendIps: node.backendIps,
        level: node.level
      })),
      connections: connections.value,
      targetNode: {
        ip: attackTarget.ip,
        port: attackTarget.port || 80
      },
      victimType: victimType.value,
      environment: environment.value,  // 添加环境参数
      selfExcitation: environment.value === 'testbed' ? selfExcitation.value : false,  // 自激选项（仅在测试床模式下有效）
      timestamp: new Date().toISOString(),
      attackId: attackDataService.generateAttackId()
    }
    
    // 发送攻击链数据
    const result = await attackDataService.sendAttackChainData(attackData)
    
    if (result.success) {
      sendingStatus.value = 'success'
      lastSendResult.value = `部署成功！攻击ID: ${result.attackId}`
      alert(`攻击链已部署到服务器！\n攻击ID: ${result.attackId}`)
        } else {
      sendingStatus.value = 'error'
      lastSendResult.value = `部署失败: ${result.message}`
      alert(`部署攻击链失败: ${result.message}`)
    }
  } catch (error) {
    console.error('生成攻击链时出错:', error)
    alert('生成攻击链时出错，请检查节点配置')
  }
}

// 解析单个域名的自增字段，例如 a.attack{i}.ns4.48232025.xyz，使用自增字段值生成域名
function parseSingleDomainPattern(domainPattern: string, incrementValue: number): string[] {
  const domains: string[] = []
  
  // 匹配 {i} 格式
  const pattern = /\{i\}/g
  const matches = [...domainPattern.matchAll(pattern)]
  
  if (matches.length === 0) {
    // 没有自增字段，直接返回原域名
    return [domainPattern.trim()]
  }
  
  // 生成从1到incrementValue的域名
  for (let i = 1; i <= incrementValue; i++) {
    const domain = domainPattern.replace(/\{i\}/g, i.toString()).trim()
    if (domain) {
      domains.push(domain)
    }
  }
  
  return domains
}

// 解析域名范围，支持换行和逗号分割多个域名，每个域名可以包含自增字段 {i}
function parseDomainRange(domainInput: string, incrementValue: number): string[] {
  const allDomains: string[] = []
  
  // 将换行符（\r\n 或 \n）替换为逗号，然后按逗号分割
  const normalized = domainInput.replace(/\r\n/g, ',').replace(/\n/g, ',')
  const segments = normalized.split(',').map(s => s.trim()).filter(s => s.length > 0)
  
  // 处理每个域名段
  for (const segment of segments) {
    try {
      const parsedDomains = parseSingleDomainPattern(segment, incrementValue)
      allDomains.push(...parsedDomains)
    } catch (error) {
      throw new Error(`域名解析错误 "${segment}": ${error instanceof Error ? error.message : '未知错误'}`)
    }
  }
  
  // 去重
  return [...new Set(allDomains)]
}

// 打开攻击配置对话框
function openAttackConfigDialog() {
  if (!configLoaded.value) {
    alert('配置尚未加载完成')
    return
  }
  
  // 检查是否有节点
  if (canvasNodes.value.length === 0) {
    alert('请先添加节点到画布')
    return
  }
  
  // 根据环境模式设置默认域名
  if (environment.value === 'testbed') {
    attackDomainRange.value = 'a.attack{i}.nsdocker.48232025.xyz'
  } else {
    attackDomainRange.value = 'a.attack{i}.ns4.48232025.xyz'
  }
  
  // 重置配置为默认值
  attackDomainIncrement.value = 1
  attackInterval.value = 0
  attackRounds.value = 1
  
  showAttackConfigDialog.value = true
}

// 确认攻击配置并开始攻击
async function confirmAttackConfig() {
  // 验证输入
  if (!attackDomainRange.value.trim()) {
    alert('请输入攻击域名范围')
    return
  }
  
  if (attackInterval.value < 0) {
    alert('每轮次攻击时间间隔不能为负数')
    return
  }
  
  if (attackRounds.value < 1 || !Number.isInteger(attackRounds.value)) {
    alert('总攻击轮次数量必须为正整数')
    return
  }
  
  // 验证自增字段值
  if (attackDomainIncrement.value < 1 || !Number.isInteger(attackDomainIncrement.value)) {
    alert('自增字段的值必须为正整数')
    return
  }
  
  // 验证域名格式
  try {
    parseDomainRange(attackDomainRange.value, attackDomainIncrement.value)
      } catch (error) {
    alert(`域名格式错误：${error instanceof Error ? error.message : '未知错误'}`)
    return
  }
  
  // 关闭对话框
  showAttackConfigDialog.value = false
  
  // 开始攻击
  await doStartAttack()
}

// 开始攻击（实际执行攻击的函数）
async function doStartAttack() {
  try {
    // 识别根节点（没有其他节点连接到其前端的节点）
    const connectedFrontendNodeIds = new Set<number>()
    connections.value.forEach(conn => {
      if (conn.to.kind === 'frontend' && conn.to.nodeId !== 'target') {
        connectedFrontendNodeIds.add(conn.to.nodeId as number)
      }
    })
    
    // 找到所有根节点（前端没有被连接的节点）
    const rootNodes = canvasNodes.value.filter(node => 
      !connectedFrontendNodeIds.has(node.id)
    )
    
    if (rootNodes.length === 0) {
      alert('未找到根节点（入口节点）。\n请确保攻击链中有至少一个没有被其他节点连接的起始节点。')
      return
    }
    
    // 获取所有根节点的前端IP
    const rootNodeIPs = rootNodes
      .map(node => node.frontendIp)
      .filter((ip): ip is string => !!ip)
    
    if (rootNodeIPs.length === 0) {
      alert('根节点缺少前端IP地址')
      return
    }
    
    console.log(`识别到 ${rootNodes.length} 个根节点:`, rootNodes.map(n => n.frontendIp || n.name))
    console.log('根节点IP列表:', rootNodeIPs)
    
    const config = await configService.getAttackChainConfig()
    const startAttackUrl = config.endpoints.startAttack
    
    console.log('发送开始攻击请求到:', startAttackUrl)
    console.log('根节点IP列表:', rootNodeIPs)
    
    // 解析攻击域名范围
    const attackDomains = parseDomainRange(attackDomainRange.value, attackDomainIncrement.value)
    
    // 构建请求数据 - 统一使用 root_node_ips 格式
    // 如果只有一个根节点，root_node_ips 数组中只包含一个IP
    const requestData: {
      root_node_ips: string[]
      victim_type: 'dns' | 'non-dns'
      attack_domains: string[]  // 攻击域名列表
      attack_interval: number   // 每轮次攻击时间间隔（秒）
      attack_rounds: number     // 总攻击轮次数量
      environment: 'testbed' | 'realnet'  // 环境类型：测试床或实网
    } = {
      root_node_ips: rootNodeIPs, // 统一使用数组格式，单个或多个根节点都使用此格式
      victim_type: victimType.value, // 添加受攻击类型
      attack_domains: attackDomains, // 攻击域名列表
      attack_interval: attackInterval.value, // 每轮次攻击时间间隔
      attack_rounds: attackRounds.value, // 总攻击轮次数量
      environment: environment.value  // 添加环境参数
    }
    
    // 创建超时控制器
    const controller = new AbortController()
    const timeoutId = setTimeout(() => {
      console.log('开始攻击请求超时，正在中止请求...')
      controller.abort()
    }, config.timeout)
    
    // POST请求到 /api/attack/start，携带根节点IP
    const response = await fetch(startAttackUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Accept': 'text/plain, application/json'
      },
      body: JSON.stringify(requestData),
      signal: controller.signal
    })
    
    // 清除超时定时器
    clearTimeout(timeoutId)
    
    if (response.ok) {
      const responseText = await response.text()
      console.log('开始攻击成功:', responseText)
      
      // 尝试解析响应，提取攻击任务ID
      let taskId = ''
      try {
        // 尝试解析为JSON
        const jsonResponse = JSON.parse(responseText)
        taskId = jsonResponse.task_id || jsonResponse.taskId || jsonResponse.id || ''
      } catch {
        // 如果不是JSON，尝试从文本中提取（可能是纯文本的任务ID）
        // 或者直接使用响应文本作为任务ID
        taskId = responseText.trim()
      }
      
      if (taskId) {
        attackTaskId.value = taskId
        console.log('攻击任务ID:', taskId)
      } else {
        console.warn('未能从响应中提取攻击任务ID')
      }
      
      // 保存攻击配置参数，供打击效果评估页面计算倍率使用
      try {
        const attackConfig = {
          domainIncrement: attackDomainIncrement.value,
          attackRounds: attackRounds.value,
          timestamp: Date.now()
        }
        localStorage.setItem('attack-config-for-amplification', JSON.stringify(attackConfig))
        console.log('已保存攻击配置用于倍率计算:', attackConfig)
      } catch (error) {
        console.error('保存攻击配置失败:', error)
      }
      
      // 注意：SSE连接已改为页面级别管理，清除操作在AttackEffectMonitor页面中处理
      // 每次进入页面时会重新创建连接，数据会自动清空
      
      const rootNodeNames = rootNodes.map(n => n.name).join(', ')
      const rootNodeIPsStr = rootNodeIPs.join(', ')
      alert(
        `攻击已开始！\n\n` +
        `根节点数量: ${rootNodes.length}\n` +
        `根节点: ${rootNodeNames}\n` +
        `根节点IP: ${rootNodeIPsStr}\n` +
        `任务ID: ${taskId || '未获取'}\n` +
        `状态: ${response.status}\n` +
        `响应: ${responseText}`
      )
      // 如果已有态势感知在运行，先停止再重新启动
      if (isMonitoring.value) {
        console.log('检测到已有状态感知在运行，先停止再重新启动')
        stopSituationAwareness()
        // 等待一小段时间确保清理完成
        await new Promise(resolve => setTimeout(resolve, 100))
      }
      // 启动态势感知
      startSituationAwareness()
    } else {
      const errorText = await response.text()
      console.error('开始攻击失败:', response.status, errorText)
      alert(`开始攻击失败！\n状态: ${response.status}\n错误: ${errorText}`)
    }
  } catch (error) {
    console.error('开始攻击失败:', error)
    if (error instanceof Error && error.name === 'AbortError') {
      alert(`开始攻击请求超时！\n请检查网络连接和服务器状态`)
    } else {
      alert(`开始攻击时出错: ${error instanceof Error ? error.message : '未知错误'}`)
    }
  }
}

// 态势感知数据类型
type SituationAwarenessData = {
  front_IP: string
  status: number  // 0=未调用, 1=已调用
  backend: Array<{
    ip: string
    status: number  // 0=未调用, 1=已调用
  }>
}

// 启动态势感知
function startSituationAwareness() {
  if (isMonitoring.value && monitoringTimer.value !== null) {
    return
  }

  // 如果之前有定时器，先清除
  if (monitoringTimer.value !== null) {
    clearInterval(monitoringTimer.value)
    monitoringTimer.value = null
  }

  isMonitoring.value = true

  // 立即调用一次
  fetchSituationAwareness()

  // 设置定时器，每隔1秒调用一次
  monitoringTimer.value = window.setInterval(() => {
    if (isMonitoring.value) {
      fetchSituationAwareness()
    }
  }, 1000)
  
  // 保存状态（包括攻击状态）
  saveState().catch(err => console.error('保存状态失败:', err))
}

// 停止态势感知（仅停止本地监控，不向后端发送请求）
function stopSituationAwareness() {
  if (!isMonitoring.value) {
    return
  }

  isMonitoring.value = false
  
  if (monitoringTimer.value !== null) {
    clearInterval(monitoringTimer.value)
    monitoringTimer.value = null
  }


  // 重置所有节点的状态，并强制触发Vue响应式更新
  canvasNodes.value.forEach(node => {
    node.frontendStatus = undefined
    node.backendStatus = undefined
  })
  
  // 强制触发Vue响应式更新，确保节点颜色恢复
  const updatedNodes = [...canvasNodes.value]
  canvasNodes.value = updatedNodes
  
  // 清除攻击任务ID
  attackTaskId.value = ''
  
  // 保存状态（更新isMonitoring为false，清除attackTaskId）
  // 注意：saveState 会自动处理 localStorage 和 IndexedDB
  saveState().catch(err => console.error('保存状态失败:', err))
}

// 获取态势感知数据
async function fetchSituationAwareness() {
  try {
    // 使用相对路径，通过Vite代理转发到后端
    const apiUrl = '/api/attack/chain_now_node'
    
    const response = await fetch(apiUrl, {
      method: 'GET',
      headers: {
        'Accept': 'application/json'
      }
    })

    if (!response.ok) {
      console.error('获取态势感知数据失败:', response.status, response.statusText)
    return
  }
  
    // 读取响应内容（一次性读取JSON格式）
    let responseText = ''
    try {
      responseText = await response.text()
    } catch (readError) {
      console.error('读取响应内容失败:', readError instanceof Error ? readError.message : String(readError))
      return
    }

    if (responseText.trim() === '') {
      return
    }
    
    // 解析JSON响应内容
    let data: SituationAwarenessData[]
    try {
      const parsed = JSON.parse(responseText)
      
      // 检查解析结果是否为数组
      if (Array.isArray(parsed)) {
        data = parsed
      } else if (typeof parsed === 'object' && parsed !== null) {
        // 如果返回的是对象，可能包含 data 字段
        if (Array.isArray(parsed.data)) {
          data = parsed.data
        } else if (Array.isArray(parsed.result)) {
          data = parsed.result
        } else {
          console.error('态势感知数据格式错误: 对象中没有data或result字段')
          return
        }
      } else {
        console.error('态势感知数据格式错误: 期望数组或对象')
    return
  }
    } catch (parseError) {
      console.error('JSON解析失败:', parseError instanceof Error ? parseError.message : String(parseError))
    return
  }
  
    if (!Array.isArray(data)) {
      console.error('态势感知数据格式错误: 期望数组')
    return
  }
  
    // 更新节点状态
    updateNodeStatus(data)
    console.log('态势感知读取数据成功')
  } catch (error) {
    console.error('获取态势感知数据出错:', error instanceof Error ? error.message : String(error))
  }
}

// 更新节点状态
function updateNodeStatus(data: SituationAwarenessData[]) {
  // 创建前端IP到节点的映射
  const ipToNodeMap = new Map<string, CanvasNode>()
  canvasNodes.value.forEach(node => {
    if (node.frontendIp) {
      ipToNodeMap.set(node.frontendIp, node)
    }
  })

  // 更新每个节点的状态
  data.forEach(item => {
    const node = ipToNodeMap.get(item.front_IP)
    if (node) {
      // 更新前端状态
      node.frontendStatus = item.status

      // 更新后端状态 - 使用普通对象以确保响应式更新
      const newBackendStatus: Record<string, number> = {}
      item.backend.forEach(backend => {
        newBackendStatus[backend.ip] = backend.status
      })
      // 替换整个对象，确保Vue能检测到变化
      node.backendStatus = newBackendStatus
    }
  })

  // 强制触发Vue响应式更新
  const updatedNodes = [...canvasNodes.value]
  canvasNodes.value = updatedNodes
}

// 获取后端状态
function getBackendStatus(node: CanvasNode, backendIndex: number): number | undefined {
  if (!node.backendStatus || !node.backendIps) {
    return undefined
  }
  const backendIp = node.backendIps[backendIndex]
  if (!backendIp) {
    return undefined
  }
  const status = node.backendStatus[backendIp]
  // 详细调试日志（仅在开发时使用，可以后续移除）
  // console.log(`getBackendStatus: 节点=${node.name}, 索引=${backendIndex}, IP=${backendIp}, 状态=${status}`)
  return status
}

// 获取后端组状态（只要有一个是绿色就显示绿色，只有全部是红色才显示红色）
// 状态值：0=未调用(红色), 1=已调用(绿色)
function getBackendGroupStatus(node: CanvasNode, group: BackendGroup): number | undefined {
  if (!node.backendStatus || !node.backendIps) {
    return undefined
  }
  
  const statuses: number[] = []
  group.backendIndices.forEach(idx => {
    const backendIp = node.backendIps![idx]
    if (backendIp) {
      const status = node.backendStatus![backendIp]
      if (status !== undefined) {
        statuses.push(status)
      }
    }
  })
  
  // 如果没有状态信息，返回undefined（灰色）
  if (statuses.length === 0) {
    return undefined
  }
  
  // 只要有一个是绿色（状态1=已调用），就返回绿色
  if (statuses.includes(1)) {
    return 1
  }
  
  // 如果全部是红色（状态0=未调用），返回红色
  if (statuses.every(s => s === 0)) {
    return 0
  }
  
  // 其他情况（比如有undefined和红色混合但没有绿色）返回undefined（灰色）
  return undefined
}

// 获取合并后端状态（用于手动拖拽的节点，合并显示时）
function getMergedBackendStatus(node: CanvasNode): number | undefined {
  if (!node.backendStatus || !node.backendIps) {
    return undefined
  }
  
  const statuses = new Set<number>()
  node.backendIps.forEach(backendIp => {
    if (backendIp) {
      const status = node.backendStatus![backendIp]
      if (status !== undefined) {
        statuses.add(status)
      }
    }
  })
  
  // 如果所有后端都是同一状态，返回该状态
  if (statuses.size === 1) {
    return Array.from(statuses)[0]
  }
  
  return undefined
}



</script>

<template>
  <div class="td-layout">
    <main class="td-canvas" ref="canvasRef" tabindex="0" @wheel="handleWheel" @mousedown="handleMouseDown">
      <!-- 图例框 -->
      <div class="legend-box">
        <div class="legend-title">
          图例<span class="legend-status">{{ isMonitoring ? '（已激活状态感知）' : '（未激活状态感知）' }}</span>
        </div>
        <div v-if="!isMonitoring" class="legend-section">
          <div class="legend-item">
            <div class="legend-dot port-status-front"></div>
            <span class="legend-text">转发节点</span>
          </div>
          <div class="legend-item">
            <div class="legend-dot port-status-clustered"></div>
            <span class="legend-text">解析节点簇</span>
          </div>
          <div class="legend-item">
            <div class="legend-dot port-status-unclustered"></div>
            <span class="legend-text">解析节点</span>
          </div>
        </div>
        <div v-else class="legend-section">
          <div class="legend-item">
            <div class="legend-dot port-status-1"></div>
            <span class="legend-text">已激活</span>
          </div>
          <div class="legend-item">
            <div class="legend-dot port-status-0"></div>
            <span class="legend-text">待激活</span>
          </div>
        </div>
        <!-- 连接线图例分隔线 -->
        <div class="legend-divider"></div>
        <!-- 连接线图例 -->
        <div class="legend-section">
          <div class="legend-item">
            <div class="legend-line legend-line-purple" style="background: linear-gradient(to right, #667eea, #764ba2);"></div>
            <span class="legend-text">传递链</span>
          </div>
          <div class="legend-item">
            <div class="legend-line legend-line-red" style="background: linear-gradient(to right, #f87171, #dc2626);"></div>
            <span class="legend-text">攻击链</span>
          </div>
          <div class="legend-item" v-if="environment === 'testbed' && selfExcitation">
            <div class="legend-line legend-line-blue-dashed" style="background: linear-gradient(to right, #60a5fa, #3b82f6);"></div>
            <span class="legend-text">自激链</span>
          </div>
        </div>
      </div>
      <div class="canvas-content" :style="{ transform: transformStyle }">
      <div v-if="canvasNodes.length === 0" class="empty">空白画布，拖拽右侧节点添加</div>
        <svg class="lines" :width="SVG_SIZE + 'px'" :height="SVG_SIZE + 'px'" :style="{ left: -SVG_OFFSET + 'px', top: -SVG_OFFSET + 'px' }">
        <g :transform="`translate(${SVG_OFFSET}, ${SVG_OFFSET})`">
          <line v-for="line in connectionLines" :key="line.key"
                :x1="line.x1"
                :y1="line.y1"
                :x2="line.x2"
                :y2="line.y2"
                :stroke="line.stroke" 
                stroke-width="3" 
                stroke-linecap="butt"
                opacity="0.9"
                vector-effect="non-scaling-stroke" />
          <line v-if="tempLine" :x1="tempLine.x1" :y1="tempLine.y1" :x2="tempLine.x2" :y2="tempLine.y2" 
                stroke="url(#gradient-blue)" stroke-width="3" stroke-dasharray="6 4" stroke-linecap="butt" opacity="0.9" 
                vector-effect="non-scaling-stroke" />
          <!-- 自激虚曲线（测试床模式且自激开启时） -->
          <path v-for="line in selfExcitationLines" :key="line.key"
                :d="line.path"
                stroke="url(#gradient-blue)" 
                stroke-width="2" 
                stroke-dasharray="8 4" 
                stroke-linecap="round"
                fill="none"
                opacity="0.6"
                vector-effect="non-scaling-stroke" />
          
          <!-- 渐变定义 -->
          <defs>
            <linearGradient id="gradient-purple" x1="0%" y1="0%" x2="100%" y2="0%">
              <stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
              <stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
            </linearGradient>
            <linearGradient id="gradient-red" x1="0%" y1="0%" x2="100%" y2="0%">
              <stop offset="0%" style="stop-color:#f87171;stop-opacity:1" />
              <stop offset="100%" style="stop-color:#dc2626;stop-opacity:1" />
            </linearGradient>
            <linearGradient id="gradient-blue" x1="0%" y1="0%" x2="100%" y2="0%">
              <stop offset="0%" style="stop-color:#60a5fa;stop-opacity:1" />
              <stop offset="100%" style="stop-color:#3b82f6;stop-opacity:1" />
            </linearGradient>
          </defs>
        </g>
      </svg>

      <!-- 重构后的节点 -->
      <div v-for="node in canvasNodes" :key="node.id" class="node-wrapper" :style="{ 
        left: node.x + 'px', 
        top: node.y + 'px'
      }"
           @mousedown.stop="onNodeMouseDown($event, node)" 
           @contextmenu="onNodeContextMenu($event, node)"
           @click.stop>
        <!-- 节点主体 -->
        <div class="node" :style="{ width: NODE_WIDTH + 'px', height: '50px' }">
          <div class="node-name">{{ node.name }}</div>
          <!-- 节点信息标签 -->
          <div class="node-info-labels">
            <span class="node-label">序号：{{ getNodeIndex(node) || '?' }}</span>
            <span class="node-label">后端：{{ node.backends }}</span>
          </div>
          <!-- 展开/收起后端按钮 -->
          <button 
            v-if="node.backends > 1"
            class="backend-toggle-btn" 
            @click="toggleBackendExpanded(node.id, $event)"
            @mousedown.stop
            :title="node.backendExpanded ? '收起后端' : '展开后端'">
            {{ node.backendExpanded ? '◀' : '▶' }}
          </button>
        </div>
        
        <!-- 前端连接点 -->
        <div class="port front-port" 
             :class="{
               'port-status-0': node.frontendStatus === 0,
               'port-status-1': node.frontendStatus === 1
             }"
             @mousedown.stop="beginLinkFrom({ nodeId: node.id, kind: 'frontend' }, $event)"
             @mouseup.stop="finishLinkTo({ nodeId: node.id, kind: 'frontend' }, $event)"
             @contextmenu.stop="deletePortConnections({ nodeId: node.id, kind: 'frontend' }, $event)"
             :title="`前端: ${node.frontendIp || '未设置'} (右键删除连接线)`">
            </div>
        
        <!-- 后端连接点 -->
        <!-- 情况1：有后端分组信息（根据聚类信息或自动编排创建） - 按分组显示 -->
        <template v-if="node.backendGroups && node.backendGroups.length > 0">
          <!-- 如果未展开，每个分组显示一个合并点 -->
          <template v-if="!node.backendExpanded">
            <div v-for="(group, groupIdx) in node.backendGroups" :key="`group-${groupIdx}`"
                 class="port back-port backend-group"
                 :class="{
                   'port-status-0': getBackendGroupStatus(node, group) === 0,
                   'port-status-1': getBackendGroupStatus(node, group) === 1,
                   'port-status-undefined': isMonitoring && getBackendGroupStatus(node, group) === undefined
                 }"
                 :style="{ top: `calc(50% + ${(groupIdx - (node.backendGroups.length - 1) / 2) * 24}px)` }"
                 @mousedown.stop="beginLinkFrom({ nodeId: node.id, kind: 'backend', backendIndex: group.backendIndices[0] }, $event)"
                 @mouseup.stop="finishLinkTo({ nodeId: node.id, kind: 'backend', backendIndex: group.backendIndices[0] }, $event)"
                 @contextmenu.stop="deletePortConnections({ nodeId: node.id, kind: 'backend', backendIndex: group.backendIndices[0] }, $event)"
                 :title="`后端组 ${groupIdx + 1}（包含 ${group.backendIndices.length} 个后端）(右键删除连接线)`">
            </div>
          </template>
          
          <!-- 如果展开，显示所有后端 -->
          <template v-if="node.backendExpanded">
        <div v-for="(_, idx) in node.backends" :key="idx" 
             class="port back-port" 
                 :class="{
                   'port-status-0': getBackendStatus(node, idx) === 0,
                   'port-status-1': getBackendStatus(node, idx) === 1,
                   'port-status-undefined': isMonitoring && getBackendStatus(node, idx) === undefined
                 }"
             :style="{ top: `calc(50% + ${(idx - (node.backends - 1) / 2) * 20}px)` }"
             @mousedown.stop="beginLinkFrom({ nodeId: node.id, kind: 'backend', backendIndex: idx }, $event)"
             @mouseup.stop="finishLinkTo({ nodeId: node.id, kind: 'backend', backendIndex: idx }, $event)"
             @contextmenu.stop="deletePortConnections({ nodeId: node.id, kind: 'backend', backendIndex: idx }, $event)"
             :title="`后端${idx + 1}: ${node.backendIps?.[idx] || '未设置'} (右键删除连接线)`">
        </div>
          </template>
        </template>
        
        <!-- 情况2：没有后端分组信息 - 显示单个合并点或所有后端 -->
        <template v-else>
          <!-- 如果未展开，只显示一个合并的后端连接点 -->
          <div v-if="!node.backendExpanded && node.backends > 0"
               class="port back-port merged-backend" 
               :class="{
                 'port-status-0': getMergedBackendStatus(node) === 0,
                 'port-status-1': getMergedBackendStatus(node) === 1,
                 'port-status-undefined': isMonitoring && getMergedBackendStatus(node) === undefined
               }"
               :style="{ top: '50%' }"
               @mousedown.stop="beginLinkFrom({ nodeId: node.id, kind: 'backend', backendIndex: 0 }, $event)"
               @mouseup.stop="finishLinkTo({ nodeId: node.id, kind: 'backend', backendIndex: 0 }, $event)"
               @contextmenu.stop="deletePortConnections({ nodeId: node.id, kind: 'backend', backendIndex: 0 }, $event)"
               :title="`后端（合并显示 ${node.backends} 个）(右键删除连接线)`">
          </div>
          
          <!-- 如果展开，显示所有后端连接点 -->
          <div v-if="node.backendExpanded"
               v-for="(_, idx) in node.backends" :key="idx" 
               class="port back-port" 
               :class="{
                 'port-status-0': getBackendStatus(node, idx) === 0,
                 'port-status-1': getBackendStatus(node, idx) === 1,
                 'port-status-undefined': isMonitoring && getBackendStatus(node, idx) === undefined
               }"
               :style="{ top: `calc(50% + ${(idx - (node.backends - 1) / 2) * 20}px)` }"
               @mousedown.stop="beginLinkFrom({ nodeId: node.id, kind: 'backend', backendIndex: idx }, $event)"
               @mouseup.stop="finishLinkTo({ nodeId: node.id, kind: 'backend', backendIndex: idx }, $event)"
               @contextmenu.stop="deletePortConnections({ nodeId: node.id, kind: 'backend', backendIndex: idx }, $event)"
               :title="`后端${idx + 1}: ${node.backendIps?.[idx] || '未设置'} (右键删除连接线)`">
          </div>
        </template>
      </div>

      <!-- 攻击目标 -->
      <div class="attack-target" :style="{ 
        left: attackTarget.x + 'px', 
        top: attackTarget.y + 'px', 
        cursor: draggingTarget ? 'grabbing' : 'grab'
      }"
           @mouseup.stop="finishLinkTo({ nodeId: 'target', kind: 'target' }, $event)" 
           @click.stop="onTargetClick"
           @mousedown.stop="onTargetMouseDown"
           @contextmenu.stop="deletePortConnections({ nodeId: 'target', kind: 'target' }, $event)"
           title="攻击目标 - 拖拽移动，点击查看攻击效果，右键删除连接线">
        <!-- 受攻击类型切换按钮 -->
        <div class="victim-type-switch" @click.stop @mousedown.stop>
          <span class="switch-label">DNS类</span>
          <div class="switch-container" @click="victimType = victimType === 'dns' ? 'non-dns' : 'dns'">
            <div class="switch-slider" :class="{ 'switch-slider-right': victimType === 'non-dns' }"></div>
          </div>
          <span class="switch-label">非DNS类</span>
        </div>
        <!-- 标靶图样 -->
        <svg class="target-icon" :width="TARGET_SIZE" :height="TARGET_SIZE" viewBox="0 0 60 60">
          <circle cx="30" cy="30" r="28" fill="#dc2626" stroke="#991b1b" stroke-width="2"/>
          <circle cx="30" cy="30" r="22" fill="#ffffff" stroke="#dc2626" stroke-width="2"/>
          <circle cx="30" cy="30" r="16" fill="#dc2626" stroke="#991b1b" stroke-width="2"/>
          <circle cx="30" cy="30" r="10" fill="#ffffff" stroke="#dc2626" stroke-width="2"/>
          <circle cx="30" cy="30" r="4" fill="#dc2626"/>
          <line x1="2" y1="30" x2="58" y2="30" stroke="#991b1b" stroke-width="1"/>
          <line x1="30" y1="2" x2="30" y2="58" stroke="#991b1b" stroke-width="1"/>
        </svg>
        <!-- IP输入框 -->
        <input 
          v-model="attackTarget.ip" 
          class="target-ip-input" 
          placeholder="目标IP"
          @click.stop
          @mousedown.stop />
      </div>
      </div>
      
      <!-- 右下角拖拽手柄 -->
    </main>

    <!-- 拖拽部署预览节点（在画布外，不受transform影响） -->
    <div v-if="draggingNodeDeploy.node" class="dragging-preview" :style="{
      left: (draggingNodeDeploy.x - NODE_WIDTH * scale / 2) + 'px',
      top: (draggingNodeDeploy.y - 25 * scale) + 'px',
      width: (NODE_WIDTH * scale) + 'px',
      height: (50 * scale) + 'px'
    }">
      <div class="node preview-node">
        <div class="node-name">{{ draggingNodeDeploy.node.name }}</div>
      </div>
    </div>

    <!-- 右侧节点列表 -->
    <aside class="td-sidebar">
      <div class="sidebar-header">
        <h3>节点列表</h3>
        <div class="header-buttons">
          <button class="header-btn" @click="addAvailableNode">新增节点</button>
          <button class="header-btn" @click="openTaskSelection">读取节点</button>
          <div class="header-btn-wrapper" ref="sortButtonWrapper">
            <button 
              class="header-btn" 
              @click="showSortMenu = !showSortMenu"
              :disabled="sortingStatus === 'sorting' || !configLoaded"
              :title="sortingStatus === 'sorting' ? '排序读取中...' : '排序读取节点'"
            >
              {{ sortingStatus === 'sorting' ? '排序读取中...' : '排序读取' }}
            </button>
            <div v-if="showSortMenu" class="sort-menu">
              <div class="sort-menu-header">选择排序方式</div>
              <button 
                class="sort-menu-item" 
                @click="sortNodes('frontend_ip_asc')"
                :disabled="sortingStatus === 'sorting'"
              >
                <span class="sort-icon">↑</span>
                前端IP正序
              </button>
              <button 
                class="sort-menu-item" 
                @click="sortNodes('frontend_ip_desc')"
                :disabled="sortingStatus === 'sorting'"
              >
                <span class="sort-icon">↓</span>
                前端IP逆序
              </button>
              <button 
                class="sort-menu-item" 
                @click="sortNodes('backend_count_asc')"
                :disabled="sortingStatus === 'sorting'"
              >
                <span class="sort-icon">↑</span>
                后端数量正序
              </button>
              <button 
                class="sort-menu-item" 
                @click="sortNodes('backend_count_desc')"
                :disabled="sortingStatus === 'sorting'"
              >
                <span class="sort-icon">↓</span>
                后端数量逆序
              </button>
              <button 
                class="sort-menu-item" 
                @click="sortNodes('node_performance_asc')"
                :disabled="sortingStatus === 'sorting'"
              >
                <span class="sort-icon">↑</span>
                节点性能正序
              </button>
              <button 
                class="sort-menu-item" 
                @click="sortNodes('node_performance_desc')"
                :disabled="sortingStatus === 'sorting'"
              >
                <span class="sort-icon">↓</span>
                节点性能逆序
              </button>
            </div>
          </div>
      </div>
      </div>
      
      <!-- 节点列表 -->
      <div 
        ref="nodeListContainer"
        class="node-list" 
        @scroll="onNodeListScroll"
      >
        <!-- 虚拟滚动：使用占位符保持总高度 -->
        <div :style="{ height: allNodes.length * NODE_ITEM_HEIGHT + 'px', position: 'relative' }">
          <!-- 只渲染可见的节点 -->
          <div 
            :style="{ 
              transform: `translateY(${visibleNodes.offsetY}px)`,
              position: 'absolute',
              top: 0,
              left: 0,
              right: 0
            }"
          >
            <div 
              v-for="(node, localIndex) in visibleNodes.nodes" 
              :key="node.id" 
             class="node-item"
             :class="{ 'deployed': node.isDeployed }"
             :data-node-index="visibleNodes.startIndex + localIndex + 1"
             @mousedown="startDragDeploy($event, node)"
              :title="node.isDeployed ? '已部署' : '拖拽到画布部署'"
              v-memo="[node.id, node.isDeployed, node.name, node.backends]"
            >
              <div class="node-index">{{ visibleNodes.startIndex + localIndex + 1 }}</div>
          <div class="node-content">
            <div class="node-info">
              <div class="node-header">
                <strong>{{ node.name }}</strong>
                <span v-if="node.isDeployed" class="deployed-badge">已部署</span>
              </div>
              <div class="node-details">
                <span class="backend-count">后端 × {{ node.backends }}</span>
              </div>
              <div v-if="node.frontendIp" class="ip-info-small">前端: {{ node.frontendIp }}</div>
              <div v-if="node.backendIps && node.backendIps.length > 0" class="backend-ips-small">
                后端: {{ node.backendIps.slice(0, 2).join(', ') }}{{ node.backendIps.length > 2 ? '...' : '' }}
              </div>
            </div>
            <div class="node-actions">
              <button class="edit-btn" @click.stop="editAvailableNode(node)" @mousedown.stop title="编辑节点">✏️</button>
              <button class="delete-btn" @click.stop="deleteAvailableNode(node.id)" @mousedown.stop title="删除节点">🗑️</button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </aside>
      
    <!-- 底部控制栏 -->
    <footer class="td-bottombar">
        <button 
        class="start-attack-btn" 
          @click="generateAttackChain"
          :disabled="sendingStatus === 'sending' || !configLoaded"
        >
          {{ sendingStatus === 'sending' ? '部署中...' : '部署攻击链' }}
        </button>
        <button 
          class="start-attack-btn" 
          @click="openAttackConfigDialog"
          :disabled="!configLoaded"
        >
          开始攻击
        </button>
        <button 
          class="situation-awareness-btn" 
          :class="{ active: isMonitoring }"
          @click="isMonitoring ? stopSituationAwareness() : startSituationAwareness()"
          :disabled="!configLoaded"
          :title="isMonitoring ? '点击停止态势感知' : '点击启动态势感知'"
        >
          状态感知
        </button>
      <button class="auto-arrange-btn" @click="autoArrangeNodes">自动编排</button>
      <button class="reset-view-btn" @click="resetCanvasView" title="整理画布：重新排列节点为对称树形结构并调整视图">整理画布</button>
          <button class="clear-canvas-btn" @click="clearCanvas" title="清空画布（取消所有部署，保留节点列表）">
            清空画布
          </button>
          <button class="clear-all-btn" @click="clearAllNodes" title="清空节点列表和画布">
            清空列表
          </button>
      
      <!-- 测试床/实网切换按钮 -->
      <div class="environment-switch" @click.stop @mousedown.stop>
        <span class="switch-label">测试床</span>
        <div class="switch-container" @click="environment = environment === 'testbed' ? 'realnet' : 'testbed'">
          <div class="switch-slider" :class="{ 'switch-slider-right': environment === 'realnet' }"></div>
        </div>
        <span class="switch-label">实网</span>
      </div>
      
      <!-- 自激选项（仅在测试床模式下显示） -->
      <div v-if="environment === 'testbed'" class="self-excitation-switch" @click.stop @mousedown.stop>
        <span class="switch-label">开启自激</span>
        <div class="switch-container" @click="selfExcitation = !selfExcitation">
          <div class="switch-slider" :class="{ 'switch-slider-right': selfExcitation }"></div>
        </div>
      </div>
      
      <div class="bottombar-info">
        <div class="server-info">
          <small>后端地址: {{ targetServerInfo || '加载配置中...' }}</small>
        </div>
      </div>
    </footer>

    <!-- 弹出窗口 -->
    <div v-if="showModal" class="modal-overlay">
      <div class="modal" @click.stop>
        <div class="modal-header">
          <!-- 关闭按钮固定在右上角 -->
          <button class="close-btn fixed-close-btn" @click="cancelAddNode" title="关闭">&times;</button>
          <h3>{{ editingNodeId !== null ? '编辑节点' : '新增节点' }}</h3>
        </div>
        <div class="modal-body">
          <div class="form-group">
            <label>节点名称:</label>
            <input v-model="modalNodeName" placeholder="请输入节点名称" />
          </div>
          <div class="form-group">
            <label>后端数量:</label>
            <input type="number" min="1" v-model.number="modalNodeBackends" />
          </div>
          <div class="form-group">
            <label>前端IP地址:</label>
            <input v-model="modalFrontendIp" placeholder="请输入前端IP地址" />
          </div>
          <div class="form-group">
            <label>后端IP地址:</label>
            <div v-for="(_, idx) in modalBackendIps" :key="idx" class="backend-ip-input">
              <label>后端 {{ idx + 1 }}:</label>
              <input v-model="modalBackendIps[idx]" :placeholder="'请输入后端' + (idx + 1) + 'IP地址'" />
            </div>
          </div>
          
          <!-- 高级参数（可选配置） -->
          <div class="server-params-section">
            <!-- 前端参数折叠按钮 -->
            <div class="expandable-section">
              <button class="expand-toggle-btn" @click="showAdvancedParams = !showAdvancedParams" type="button">
                <span class="expand-icon">{{ showAdvancedParams ? '▼' : '▶' }}</span>
                前端参数（高级）
              </button>
              <div v-show="showAdvancedParams" class="expandable-content">
            <div class="form-group">
              <label>RD敏感标志:</label>
              <select v-model="modalRdSenstiveFlage">
                <option :value="undefined">未设置</option>
                <option :value="true">是</option>
                <option :value="false">否</option>
              </select>
            </div>
            <div class="form-group">
              <label>负缓存:</label>
              <input type="number" v-model.number="modalNegativeCache" placeholder="留空表示未设置" />
            </div>
            <div class="form-group">
              <label>前端查询合并时间窗口:</label>
              <input type="number" v-model.number="modalFrontendQueryMergeTimeWindow" placeholder="留空表示未设置" />
                </div>
                <div class="form-group">
                  <label>整体权威端持续时间（秒）:</label>
                  <input type="number" v-model.number="modalOverallAuthDuration" placeholder="留空表示未设置" step="0.1" />
                </div>
                <div class="form-group">
                  <label>整体受攻击端持续时间（秒）:</label>
                  <input type="number" v-model.number="modalOverallVicDuration" placeholder="留空表示未设置" step="0.1" />
                </div>
                <div class="form-group">
                  <label>权威服务器请求总次数:</label>
                  <input type="number" v-model.number="modalTotalRequestAuthTimes" placeholder="留空表示未设置" />
                </div>
                <div class="form-group">
                  <label>受攻击服务器请求总次数:</label>
                  <input type="number" v-model.number="modalTotalRequestVicTimes" placeholder="留空表示未设置" />
                </div>
              </div>
            </div>
            
            <!-- 后端详细配置折叠按钮 -->
            <div class="expandable-section">
              <button class="expand-toggle-btn" @click="showBackendConfigs = !showBackendConfigs" type="button">
                <span class="expand-icon">{{ showBackendConfigs ? '▼' : '▶' }}</span>
                后端参数（高级）
              </button>
              <div v-show="showBackendConfigs" class="expandable-content backend-configs">
              <div v-for="(config, idx) in modalBackendConfigs" :key="idx" class="backend-config-item">
                <div class="backend-config-header">后端 {{ idx + 1 }}: {{ config.ip }}</div>
                <div class="backend-config-form">
                  <div class="form-group compact">
                    <label>A记录查询次数:</label>
                    <input type="number" v-model.number="config.query_A_times" />
                  </div>
                  <div class="form-group compact">
                    <label>AAAA记录查询次数:</label>
                    <input type="number" v-model.number="config.query_AAAA_times" />
                  </div>
                  <div class="form-group compact">
                    <label>重试总时间窗口:</label>
                    <input type="number" v-model.number="config.retry_total_time_window" placeholder="可选" />
                  </div>
                  <div class="form-group compact">
                    <label>最大扇出数:</label>
                    <input type="number" v-model.number="config.max_fanout_num" placeholder="可选" />
                  </div>
                  <div class="form-group compact">
                    <label>后端查询合并时间窗口:</label>
                    <input type="number" v-model.number="config.backend_query_merge_time_window" placeholder="可选" />
                  </div>
                    <div class="form-group compact">
                      <label>向权威服务器发送查询次数:</label>
                      <input type="number" v-model.number="config.request_auth_times" placeholder="可选" />
                </div>
                    <div class="form-group compact">
                      <label>权威服务器时间索引:</label>
                      <input type="number" v-model.number="config.auth_time_index" placeholder="可选" step="0.1" />
              </div>
                    <div class="form-group compact">
                      <label>向受攻击服务器发送查询次数:</label>
                      <input type="number" v-model.number="config.request_vic_times" placeholder="可选" />
            </div>
                    <div class="form-group compact">
                      <label>受攻击服务器时间索引:</label>
                      <input type="number" v-model.number="config.vic_time_index" placeholder="可选" step="0.1" />
                    </div>
                    <div class="form-group compact">
                      <label>聚类分类:</label>
                      <input type="number" v-model.number="config.cluster_group" placeholder="例如 0, 1, 2..." min="0" />
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div class="modal-footer">
          <button class="btn-cancel" @click="cancelAddNode">取消</button>
          <button class="btn-confirm" @click="confirmAddNode">{{ editingNodeId !== null ? '保存' : '确认' }}</button>
        </div>
      </div>
    </div>

    <!-- 任务选择窗口 -->
    <div v-if="showTaskSelectionModal" class="modal-overlay">
      <div class="modal task-modal" @click.stop>
        <div class="modal-header">
          <h3>从服务器读取节点</h3>
          <button class="close-btn" @click="closeTaskSelection">&times;</button>
        </div>
        <div class="modal-body">
          <!-- 实网模式：显示任务列表 -->
          <template v-if="environment === 'realnet'">
          <div v-if="loadingTasks" class="loading-indicator">
            <span>正在加载任务列表...</span>
          </div>
          <div v-else-if="taskList.length === 0" class="empty-tasks">
            <span>暂无可用任务</span>
          </div>
          <div v-else class="task-list-container">
            <div class="task-list-header">
              <span>请选择要读取的任务（可多选）：</span>
            </div>
            <div class="task-list">
              <div v-for="task in taskList" :key="task.id" class="task-item">
                <label class="task-checkbox">
                  <input 
                    type="checkbox" 
                    :value="task.task_id" 
                    v-model="selectedTaskIds"
                  />
                  <div class="task-info">
                    <div class="task-id">任务 ID: {{ task.task_id }}</div>
                    <div class="task-details">
                        <span class="task-time">{{ task.start_time }} ~ {{ task.end_time || '进行中' }}</span>
                        <span class="task-status" :class="getTaskStatusClass(task.status)">{{ formatTaskStatus(task.status) }}</span>
                        <span v-if="task.node_count !== undefined" class="task-node-count">节点数: {{ task.node_count }}</span>
                    </div>
                  </div>
                </label>
              </div>
            </div>
            </div>
          </template>
          
          <!-- 测试床模式：不显示任务列表，只显示提示 -->
          <template v-else>
            <div class="testbed-hint" style="padding: 16px; background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 8px; margin-bottom: 16px;">
              <p style="margin: 0; color: #0369a1; font-size: 14px;">
                <strong>测试床模式：</strong>将直接读取测试床节点，无需选择任务。
              </p>
            </div>
          </template>
          <div class="config-section" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e5e7eb;">
            <div class="config-field">
              <label>后端聚类数量 <span style="color: #ef4444;">*</span></label>
              <input 
                type="number" 
                min="1" 
                required
                v-model.number="clusterCount" 
                placeholder="例如 4（必填）"
                style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;"
              />
            </div>
            <p class="config-hint" style="margin-top: 8px; font-size: 12px; color: #6b7280;">
              将后端节点聚类为指定数量的类别。
            </p>
          </div>
        </div>
        <div class="modal-footer">
          <button class="btn-cancel" @click="closeTaskSelection">取消</button>
          <button 
            class="btn-confirm" 
            @click="loadNodesFromTasks"
            :disabled="loadingNodes || (environment === 'realnet' && selectedTaskIds.length === 0) || !clusterCount || clusterCount < 1"
          >
            {{ loadingNodes ? '读取中...' : (environment === 'testbed' ? '读取节点' : `读取节点 (${selectedTaskIds.length})`) }}
          </button>
        </div>
      </div>
    </div>

    <!-- 配置选项窗口 -->
    <div v-if="showConfigModal" class="modal-overlay">
      <div class="modal config-modal" @click.stop>
        <div class="modal-header">
          <h3>配置选项</h3>
          <button class="close-btn" @click="closeConfigModal">&times;</button>
        </div>
        <div class="modal-body">
          <div class="config-section">
            <div class="config-row">
              <div class="config-field">
                <label>攻击链跳数</label>
                <input 
                  type="number" 
                  min="1" 
                  v-model.number="configDepth" 
                />
            </div>
              <div class="config-field">
                <label>攻击链最大度数</label>
                <input 
                  type="number" 
                  min="1" 
                  :max="clusterCount || undefined"
                  v-model.number="configFanout" 
                  :placeholder="clusterCount ? `不能大于后端聚类数量${clusterCount}` : '请先设置后端聚类数量'"
                />
              </div>
              <div class="config-field">
                <label>攻击链条数</label>
                <input 
                  type="number" 
                  min="1" 
                  v-model.number="chainCount" 
                  placeholder="例如 1, 2, 3..."
                />
            </div>
            </div>
            <p class="config-hint">
              设置自动编排的攻击链跳数、攻击链最大度数和攻击链条数，后端据此补全拓扑结构。多条攻击链将生成多个根节点。
            </p>
          </div>
          
          <!-- 攻击链优化栏 -->
          <div class="config-section">
            <div class="config-section-header">
              <label>攻击链优化</label>
              <label class="toggle-switch" @click.stop>
                <input 
                  type="checkbox" 
                  v-model="optimizeEnabled"
                />
                <span class="toggle-slider"></span>
              </label>
            </div>
            <div class="optimize-item-wrapper">
              <div 
                class="optimize-header"
                @click="optimizeExpanded = !optimizeExpanded"
              >
                <span class="expand-icon" :class="{ expanded: optimizeExpanded }">
                  ▶
                </span>
                <span class="optimize-header-text">优化项</span>
              </div>
              <div v-show="optimizeExpanded" class="optimize-items">
                <div class="optimize-item">
                  <span class="optimize-name">质量初筛：剔除低效节点</span>
                  <div class="info-icon-wrapper">
                    <span class="info-icon">i</span>
                    <div class="info-tooltip">
                      仅保留后端A和AAAA记录查询活跃的节点，从源头确保基础解析能力。
                    </div>
                  </div>
                </div>
                <div class="optimize-item">
                  <span class="optimize-name">智能排序：优选核心节点</span>
                  <div class="info-icon-wrapper">
                    <span class="info-icon">i</span>
                    <div class="info-tooltip">
                      综合后端数量、可用率与活跃度排序节点优先级，按优先级编排攻击链。
                    </div>
                  </div>
                </div>
                <div class="optimize-item">
                  <span class="optimize-name">聚类均衡：分散有效节点</span>
                  <div class="info-icon-wrapper">
                    <span class="info-icon">i</span>
                    <div class="info-tooltip">
                      将有效节点分散至各聚类，平均各聚类被转发器调度的概率，维持链结构均衡。
                    </div>
                  </div>
                </div>
                <div class="optimize-item">
                  <span class="optimize-name">资源隔离：构建无冲突链</span>
                  <div class="info-icon-wrapper">
                    <span class="info-icon">i</span>
                    <div class="info-tooltip">
                      已用于构建攻击链的转发器及解析器节点被排除于编排候选列表，保证链无内环结构。
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
          
          <!-- 国家选择区域（仅在实网模式下显示） -->
          <div v-if="environment === 'realnet'" class="config-section">
            <div class="config-section-header">
              <label>选择国家</label>
              <span class="selected-count" v-if="selectedCountries.size > 0">
                已选择 {{ selectedCountries.size }} 个国家
              </span>
            </div>
            <div class="country-selector">
              <div v-if="loadingCountries" class="loading-countries">
                正在加载国家列表...
            </div>
              <div v-else-if="continentGroups.length === 0" class="no-countries">
                暂无可用国家
          </div>
              <div v-else class="continent-list">
                <div 
                  v-for="continent in continentGroups" 
                  :key="continent.continentName"
                  class="continent-item"
                >
                  <div 
                    class="continent-header"
                    @click="toggleContinent(continent.continentName)"
                  >
                    <span class="expand-icon" :class="{ expanded: expandedContinents.has(continent.continentName) }">
                      ▶
                  </span>
                    <span class="continent-name">{{ continent.continentNameZh }}</span>
                    <span class="continent-count">({{ continent.countries.length }})</span>
                    <label 
                      class="continent-checkbox"
                      @click.stop="toggleContinentCountries(continent)"
                    >
                      <input 
                        type="checkbox" 
                        :checked="isContinentAllSelected(continent)"
                        @change="toggleContinentCountries(continent)"
                      />
                      <span>全选</span>
                    </label>
                </div>
                  <div 
                    v-show="expandedContinents.has(continent.continentName)"
                    class="country-list"
                  >
                    <label 
                      v-for="country in continent.countries"
                      :key="country.name"
                      class="country-item"
                    >
                      <input 
                        type="checkbox"
                        :checked="isCountrySelected(country.name)"
                        @change="toggleCountry(country.name)"
                      />
                      <span class="country-name">{{ country.nameZh }}</span>
                      <span class="country-name-en">({{ country.name }})</span>
                    </label>
              </div>
            </div>
                </div>
              </div>
            </div>
          
          <div 
            v-if="configMessage" 
            :class="['config-message', configStatus]"
          >
            {{ configMessage }}
          </div>
          <div 
            v-if="clusterMessage" 
            :class="['config-message', clusterStatus]"
          >
            {{ clusterMessage }}
        </div>

          <div class="config-actions">
            <button 
              class="config-btn" 
              @click="executeAutoArrange"
              :disabled="configStatus === 'saving' || clusterStatus === 'saving'"
            >
              {{ (configStatus === 'saving' || clusterStatus === 'saving') ? '编排中...' : '编排' }}
                </button>
              </div>
            </div>
                </div>
              </div>

    <!-- 攻击配置对话框 -->
    <div v-if="showAttackConfigDialog" class="dialog-mask" @click.self="showAttackConfigDialog = false">
      <div class="attack-config-dialog">
        <div class="dialog-header">
          <h3>攻击配置</h3>
          <div class="dialog-subtitle">配置攻击参数</div>
            </div>
        <div class="dialog-content">
          <div class="form-row">
            <label>攻击域名范围</label>
            <textarea 
              v-model="attackDomainRange" 
              placeholder="例如：a.attack{i}.ns4.48232025.xyz&#10;或：&#10;a.attack1.ns4.48232025.xyz, a.attack2.ns4.48232025.xyz&#10;或：&#10;test.example.com"
              rows="5"
              class="domain-input"
            ></textarea>
            <div class="input-hint">
              域名输入规则：<br>
              1. 支持换行或逗号（,）分割多个域名<br>
              2. 支持自增字段：使用 {i} 格式表示自增占位符，配合下方"自增字段"值使用<br>
              3. 例如：域名为 a.attack{i}.ns4.48232025.xyz，自增字段值为5，则生成 a.attack1.ns4.48232025.xyz 到 a.attack5.ns4.48232025.xyz 共5个域名<br>
              4. 可以混合使用自增字段和普通域名
          </div>
        </div>
          <div class="form-row">
            <label>自增字段</label>
            <input 
              type="number" 
              v-model.number="attackDomainIncrement" 
              placeholder="10"
              min="1"
              class="increment-input"
            />
            <div class="input-hint">
              自增字段的值：当域名中包含 {i} 时，将从1开始自增到此值。例如输入5，则生成从1到5的域名。
            </div>
          </div>
          <div class="form-row">
            <label>每轮次攻击时间间隔（秒）</label>
            <input 
              type="number" 
              v-model.number="attackInterval" 
              placeholder="0"
              min="0"
              class="interval-input"
            />
          </div>
          <div class="form-row">
            <label>总攻击轮次数量</label>
            <input 
              type="number" 
              v-model.number="attackRounds" 
              placeholder="1"
              min="1"
              class="rounds-input"
            />
          </div>
        </div>
        <div class="dialog-actions">
          <button class="btn-cancel" @click="showAttackConfigDialog = false">取消</button>
          <button class="btn-confirm" @click="confirmAttackConfig">确定</button>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.td-layout { 
  position: fixed; 
  top: 44px; /* 避开Layout标题栏 */
  right: 260px; /* 调整以适应序号显示 */
  bottom: 60px; /* 为底部栏留出空间 */
  left: 193px; /* 180px(菜单宽度) + 1px(菜单右边框) + 12px(content的padding) */
  display: flex; 
  overflow: hidden; /* 改为hidden，避免不必要的滚动条 */
  z-index: 1; 
}

.td-canvas { 
  position: fixed; 
  top: 44px; /* 避开Layout标题栏 */
  left: 193px; /* 180px(菜单宽度) + 1px(菜单右边框) + 12px(content的padding) */
  right: 260px; /* 为右侧节点列表留出空间，调整以适应序号显示 */
  bottom: 60px; /* 为底部栏留出空间 */ 
  background: 
    radial-gradient(circle at 20% 20%, rgba(102, 126, 234, 0.08) 0%, transparent 50%),
    radial-gradient(circle at 80% 80%, rgba(118, 75, 162, 0.08) 0%, transparent 50%),
    repeating-linear-gradient(0deg, transparent, transparent 19px, rgba(229, 231, 235, 0.3) 19px, rgba(229, 231, 235, 0.3) 20px),
    repeating-linear-gradient(90deg, transparent, transparent 19px, rgba(229, 231, 235, 0.3) 19px, rgba(229, 231, 235, 0.3) 20px),
    #fafafa;
  border: none; 
  border-radius: 0; 
  overflow: hidden; 
  user-select: none; 
  outline: none; 
  margin: 0; 
  flex-shrink: 0; 
  transform-origin: 0 0;
  z-index: 1;
  cursor: grab;
}

.td-canvas:active {
  cursor: grabbing;
}

.canvas-content {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
.lines { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.empty { 
  position: absolute; 
  inset: 0; 
  display: flex; 
  align-items: center; 
  justify-content: center; 
  color: #667eea; 
  font-size: 16px; 
  font-weight: 500;
  letter-spacing: 0.5px;
  text-shadow: 0 2px 4px rgba(102, 126, 234, 0.1);
  pointer-events: none; /* 允许鼠标事件穿透，不阻挡画布拖拽 */
}

/* 节点包装器 */
.node-wrapper {
  position: absolute;
  z-index: 1;
  user-select: none;
}

/* 节点序号徽章 */
.node-index-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 20px;
  height: 18px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  font-size: 11px;
  font-weight: 600;
  border-radius: 10px;
  padding: 2px 6px;
  margin-top: 2px;
  box-shadow: 0 1px 3px rgba(102, 126, 234, 0.3);
  pointer-events: none; /* 不阻挡节点的拖拽 */
}

/* 拖拽预览节点 */
.dragging-preview {
  position: fixed;
  z-index: 9999;
  pointer-events: none;
  opacity: 0.7;
}

.preview-node {
  width: 100% !important;
  height: 100% !important;
  cursor: grabbing !important;
  box-shadow: 
    0 8px 24px rgba(102, 126, 234, 0.3),
    0 4px 8px rgba(0, 0, 0, 0.2) !important;
  border: 2px dashed #667eea;
}

/* 节点主体 */
.node { 
  background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); 
  border: 2px solid #667eea40;
  border-radius: 20px;
  box-shadow: 
    0 4px 6px rgba(102, 126, 234, 0.1),
    0 1px 3px rgba(0, 0, 0, 0.08),
    inset 0 1px 0 rgba(255, 255, 255, 0.8);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 12px 8px;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  position: relative;
  overflow: hidden;
}

.node::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 20px;
  padding: 2px;
  background: linear-gradient(135deg, #667eea, #764ba2);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask-composite: exclude;
  opacity: 0;
  transition: opacity 0.3s;
  pointer-events: none;
}

.node:hover {
  border-color: #667eea60;
  box-shadow: 
    0 8px 16px rgba(102, 126, 234, 0.2),
    0 4px 6px rgba(0, 0, 0, 0.1),
    inset 0 1px 0 rgba(255, 255, 255, 1);
  transform: translateY(-2px);
}

.node:hover::before {
  opacity: 1;
}

.node-name {
  font-size: 13px;
  font-weight: 600;
  color: #1f2937;
  text-align: center;
  cursor: move;
  letter-spacing: 0.3px;
  text-shadow: 0 1px 2px rgba(255, 255, 255, 0.8);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  padding: 0 8px;
}

.node-info-labels {
  display: flex;
  gap: 6px;
  justify-content: center;
  align-items: center;
  margin-top: 2px;
}

.node-label {
  font-size: 9px;
  color: #6b7280;
  background: rgba(255, 255, 255, 0.8);
  padding: 1px 4px;
  border-radius: 3px;
  border: 1px solid rgba(102, 126, 234, 0.2);
  white-space: nowrap;
  font-weight: 500;
}

/* 连接点样式 */
.port {
  position: absolute;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  cursor: pointer; 
  pointer-events: auto;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  border: 3px solid #fff;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  z-index: 2;
}

.front-port {
  background: linear-gradient(135deg, #3b82f6, #2563eb); /* 蓝色：解析器 */
  border-color: #fff;
  left: -16px;  /* 圆点宽16px，左边缘在-16px，中心在-8px */
  top: 50%;
  transform: translateY(-50%);
}

.front-port:hover {
  background: linear-gradient(135deg, #2563eb, #1d4ed8);
  transform: translateY(-50%) scale(1.4);
  box-shadow: 
    0 4px 12px rgba(59, 130, 246, 0.4),
    0 0 0 6px rgba(59, 130, 246, 0.3);
}

.back-port {
  background: linear-gradient(135deg, #f59e0b, #d97706); /* 改为橙色，与前端蓝色和状态感知绿色区分 */
  border-color: #fff;
  right: -16px;  /* 圆点宽16px，右边缘在-16px，中心在-8px */
  transform: translateY(-50%);
}

.back-port:hover {
  background: linear-gradient(135deg, #d97706, #b45309);
  transform: translateY(-50%) scale(1.4);
  box-shadow: 
    0 4px 12px rgba(245, 158, 11, 0.4),
    0 0 0 6px rgba(245, 158, 11, 0.3);
}

/* 合并后端连接点样式（手动拖拽的节点） */
.merged-backend {
  background: linear-gradient(135deg, #f59e0b, #d97706) !important;
  box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.3);
}

.merged-backend:hover {
  background: linear-gradient(135deg, #d97706, #b45309) !important;
  transform: translateY(-50%) scale(1.4);
  box-shadow: 
    0 4px 12px rgba(245, 158, 11, 0.5),
    0 0 0 6px rgba(245, 158, 11, 0.3) !important;
}

/* 态势感知状态颜色 - 使用更高优先级的选择器确保覆盖其他样式 */
.port.port-status-0,
.back-port.port-status-0,
.merged-backend.port-status-0,
.backend-group.port-status-0 {
  background: #ef4444 !important; /* 红色：未调用 */
  box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.3) !important;
}

.port.port-status-1,
.back-port.port-status-1,
.merged-backend.port-status-1,
.backend-group.port-status-1 {
  background: #22c55e !important; /* 绿色：已调用 */
  box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.3) !important;
}

.port.port-status-0:hover,
.back-port.port-status-0:hover,
.merged-backend.port-status-0:hover,
.backend-group.port-status-0:hover {
  background: #dc2626 !important;
}

.port.port-status-1:hover,
.back-port.port-status-1:hover,
.merged-backend.port-status-1:hover,
.backend-group.port-status-1:hover {
  background: #16a34a !important;
}

/* 态势感知未收到数据状态 - 灰色 */
.port.port-status-undefined,
.back-port.port-status-undefined,
.merged-backend.port-status-undefined,
.backend-group.port-status-undefined {
  background: #9ca3af !important; /* 灰色：未收到数据 */
  box-shadow: 0 0 0 2px rgba(156, 163, 175, 0.3) !important;
}

.port.port-status-undefined:hover,
.back-port.port-status-undefined:hover,
.merged-backend.port-status-undefined:hover,
.backend-group.port-status-undefined:hover {
  background: #6b7280 !important;
}

/* 后端分组连接点样式（自动编排的节点） */
.backend-group {
  background: linear-gradient(135deg, #8b5cf6, #7c3aed) !important;
  box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.3);
}

.backend-group:hover {
  background: linear-gradient(135deg, #7c3aed, #6d28d9) !important;
  transform: translateY(-50%) scale(1.4);
  box-shadow: 
    0 4px 12px rgba(139, 92, 246, 0.5),
    0 0 0 6px rgba(139, 92, 246, 0.3) !important;
}

/* 图例框 */
.legend-box {
  position: absolute;
  top: 12px;
  left: 12px;
  background: rgba(255, 255, 255, 0.95);
  border: 1px solid rgba(102, 126, 234, 0.2);
  border-radius: 8px;
  padding: 12px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  z-index: 100;
  min-width: 160px;
  backdrop-filter: blur(10px);
}

.legend-title {
  font-size: 13px;
  font-weight: 600;
  color: #1e293b;
  margin-bottom: 8px;
  padding-bottom: 6px;
  border-bottom: 1px solid rgba(102, 126, 234, 0.1);
}

.legend-status {
  font-size: 11px;
  font-weight: 400;
  color: #64748b;
  margin-left: 4px;
}

.legend-section {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.legend-item {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 8px;
  height: 22px;
  line-height: 22px;
}

.legend-dot {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  border: 3px solid #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
  flex-shrink: 0;
  pointer-events: none;
  transition: none !important;
  margin: 0;
  padding: 0;
  align-self: center; /* 确保在 flex 容器中垂直居中 */
}

.legend-divider {
  height: 1px;
  background: rgba(102, 126, 234, 0.1);
  margin: 8px 0;
}

.legend-line {
  flex-shrink: 0;
  width: 40px;
  height: 3px;
  margin: 0;
  padding: 0;
  opacity: 0.9;
  border-radius: 1.5px;
  display: block;
}

.legend-line-blue-dashed {
  mask-image: repeating-linear-gradient(
    to right,
    transparent 0,
    transparent 3px,
    black 3px,
    black 13px
  );
  -webkit-mask-image: repeating-linear-gradient(
    to right,
    transparent 0,
    transparent 3px,
    black 3px,
    black 13px
  );
}

.legend-dot.port-status-front {
  background: #3b82f6; /* 蓝色：解析器 */
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15), 0 0 0 2px rgba(59, 130, 246, 0.3);
}

.legend-dot.port-status-clustered {
  background: #8b5cf6; /* 紫色：已聚类转发器 */
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15), 0 0 0 2px rgba(139, 92, 246, 0.3);
}

.legend-dot.port-status-unclustered {
  background: #f59e0b; /* 橙色：未聚类转发器 */
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15), 0 0 0 2px rgba(245, 158, 11, 0.3);
}

.legend-dot.port-status-0 {
  background: #ef4444;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15), 0 0 0 2px rgba(239, 68, 68, 0.3);
}

.legend-dot.port-status-1 {
  background: #22c55e;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15), 0 0 0 2px rgba(34, 197, 94, 0.3);
}

.legend-dot.port-status-undefined {
  background: #9ca3af;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15), 0 0 0 2px rgba(156, 163, 175, 0.3);
}

.legend-text {
  font-size: 12px;
  color: #475569;
  line-height: 22px;
  margin: 0;
  padding: 0;
  display: flex;
  align-items: center;
  height: 22px;
}

/* 后端展开/收起按钮 */
.backend-toggle-btn {
  position: absolute;
  top: 3px;
  right: 3px;
  width: 18px;
  height: 18px;
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  border: none;
  border-radius: 3px;
  font-size: 10px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s ease;
  box-shadow: 0 1px 3px rgba(102, 126, 234, 0.3);
  z-index: 10;
}

.backend-toggle-btn:hover {
  background: linear-gradient(135deg, #764ba2, #667eea);
  transform: scale(1.1);
  box-shadow: 0 2px 6px rgba(102, 126, 234, 0.4);
}

.backend-toggle-btn:active {
  transform: scale(0.95);
}

.td-sidebar { 
  width: 260px; /* 调整以适应序号显示 */
  flex-shrink: 0; 
  border-left: 2px solid #e5e7eb; 
  padding: 10px; /* 优化内边距 */
  overflow: hidden; /* 改为 hidden，让内部容器处理滚动 */
  position: fixed; 
  top: 44px; /* 避开Layout标题栏（12px padding * 2 + 20px字体 ≈ 44px） */
  right: 0; 
  bottom: 60px; /* 为底部栏留出空间 */
  z-index: 10; 
  background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%);
  box-shadow: -2px 0 8px rgba(0, 0, 0, 0.05);
  display: flex;
  flex-direction: column;
}

.sidebar-header {
  display: flex;
  flex-direction: column;
  margin-bottom: 16px;
  padding: 12px;
  background: linear-gradient(135deg, #667eea15, #764ba215);
  border-radius: 8px;
  border: 1px solid #e5e7eb;
}

.sidebar-header h3 {
  margin: 0 0 8px 0;
  font-size: 15px;
  font-weight: 700;
  background: linear-gradient(135deg, #667eea, #764ba2);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

/* 头部按钮容器 - 重新设计 */
.header-buttons {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 8px;
  margin-top: 0;
  width: 100%;
  box-sizing: border-box;
}

/* 统一的按钮样式 */
.header-btn {
  border: none;
  padding: 8px 6px;
  border-radius: 8px;
  font-size: 12px;
  font-weight: 500; 
  cursor: pointer;
  transition: all 0.2s ease;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
  outline: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  box-sizing: border-box;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-width: 0;
}

.header-btn:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
}

.header-btn:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
}

.header-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* 排序按钮包装器 - 用于定位下拉菜单 */
.header-btn-wrapper {
  position: relative;
  display: flex;
  width: 100%;
  min-width: 0;
}

.sort-menu {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: 4px;
  background: white;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  z-index: 1000;
  overflow: hidden;
  min-width: 180px;
  width: auto;
  white-space: nowrap;
  box-sizing: border-box;
}

.sort-menu-header {
  padding: 10px 16px;
  font-size: 13px;
  font-weight: 600;
  color: #64748b;
  background: #f8fafc;
  border-bottom: 1px solid #e5e7eb;
  white-space: nowrap;
}

.sort-menu-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  min-width: 180px;
  padding: 12px 16px;
  border: none;
  background: white;
  text-align: left;
  font-size: 14px;
  color: #1e293b;
  cursor: pointer;
  transition: background 0.2s ease;
  white-space: nowrap;
}

.sort-menu-item:hover:not(:disabled) {
  background: #f8fafc;
}

.sort-menu-item:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.sort-menu-item:not(:last-child) {
  border-bottom: 1px solid #f1f5f9;
}

.sort-icon {
  font-size: 14px;
  color: #667eea;
  font-weight: bold;
  width: 16px;
  text-align: center;
}

/* 底部控制栏样式 */
.td-bottombar {
  position: fixed;
  left: 193px; /* 180px(菜单宽度) + 1px(菜单右边框) + 12px(content的padding) */
  right: 0;
  bottom: 0;
  height: 60px;
  background: linear-gradient(180deg, #ffffff 0%, #f9fafb 100%);
  border-top: 2px solid #e5e7eb;
  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
  display: flex;
  gap: 10px;
  padding: 10px 16px 10px 20px; /* 左侧增加padding */
  align-items: center;
  z-index: 15;
  overflow-x: auto;
}

.td-bottombar button {
  flex-shrink: 0;
  white-space: nowrap;
  width: 120px;
  height: 36px;
  padding: 0 12px;
  border: none; 
  border-radius: 8px; 
  font-size: 13px;
  font-weight: 500; 
  cursor: pointer;
  transition: all 0.2s ease;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
  outline: none;  /* 移除焦点时的黑边 */
}

.td-bottombar button:focus {
  outline: none;  /* 确保焦点状态也没有黑边 */
}

.td-bottombar button:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
}

.td-bottombar button:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
}

.td-bottombar button:disabled {
  background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%);
  cursor: not-allowed;
  opacity: 0.6;
}

/* 态势感知按钮激活状态 */
.situation-awareness-btn.active {
  background: linear-gradient(135deg, #10b981 0%, #059669 100%);
  color: white;
  font-weight: 600;
  box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
  animation: pulse-green 2s ease-in-out infinite;
}

.situation-awareness-btn.active:hover:not(:disabled) {
  background: linear-gradient(135deg, #059669 0%, #047857 100%);
  box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
}

@keyframes pulse-green {
  0%, 100% {
    box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
  }
  50% {
    box-shadow: 0 4px 16px rgba(16, 185, 129, 0.5);
  }
}

.bottombar-info {
  display: flex;
  gap: 12px;
  align-items: center;
  margin-left: auto;
  flex-shrink: 0;
}

/* 测试床/实网切换按钮样式（参考受攻击类型切换按钮，但更大且无背景框） */
.environment-switch {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: 12px;
  margin-right: 12px;
  z-index: 10;
  position: relative;
  min-width: 160px;
  width: auto;
  cursor: pointer;
}

.environment-switch .switch-label {
  font-size: 13px;
  color: #374151;
  font-weight: 500;
  padding: 0 4px;
  min-width: fit-content;
  white-space: nowrap;
}

.environment-switch .switch-container {
  position: relative;
  width: 48px;
  height: 24px;
  background: #d1d5db;
  border-radius: 12px;
  cursor: pointer;
  transition: background 0.3s;
  flex-shrink: 0;
}

.environment-switch .switch-container:hover {
  background: #9ca3af;
}

.environment-switch:has(.switch-slider-right) .switch-container {
  background: #3b82f6;
}

.environment-switch:has(.switch-slider-right) .switch-container:hover {
  background: #2563eb;
}

.environment-switch .switch-slider {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  background: #fff;
  border-radius: 50%;
  transition: transform 0.3s;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

.environment-switch .switch-slider-right {
  transform: translateX(24px);
}

/* 自激选项开关样式 */
.self-excitation-switch {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: 12px;
  margin-right: 12px;
  z-index: 10;
  position: relative;
  min-width: 120px;
  width: auto;
  cursor: pointer;
}

.self-excitation-switch .switch-label {
  font-size: 13px;
  color: #374151;
  font-weight: 500;
  padding: 0 4px;
  min-width: fit-content;
  white-space: nowrap;
}

.self-excitation-switch .switch-container {
  position: relative;
  width: 48px;
  height: 24px;
  background: #d1d5db;
  border-radius: 12px;
  cursor: pointer;
  transition: background 0.3s;
  flex-shrink: 0;
}

.self-excitation-switch .switch-container:hover {
  background: #9ca3af;
}

.self-excitation-switch:has(.switch-slider-right) .switch-container {
  background: #3b82f6;
}

.self-excitation-switch:has(.switch-slider-right) .switch-container:hover {
  background: #2563eb;
}

.self-excitation-switch .switch-slider {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  background: #fff;
  border-radius: 50%;
  transition: transform 0.3s;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

.self-excitation-switch .switch-slider-right {
  transform: translateX(24px);
}

.form { display: grid; gap: 0px; margin-bottom: 12px; }
/* 底部栏按钮样式已统一到 .td-bottombar button */
.inline { display: flex; align-items: center; gap: 8px; }
input { border: 1px solid #d1d5db; border-radius: 6px; padding: 6px 8px; }
.primary { 
  background: #3b82f6; 
  color: #fff; 
  border: none; 
  padding: 8px 16px; 
  border-radius: 6px; 
  cursor: pointer; 
  font-size: 14px;
  font-weight: 500; 
  width: 100%;
  margin-top: 8px;
  transition: all 0.2s ease;
}
.primary:hover { 
  background: #2563eb; 
  transform: translateY(-1px);
  box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3);
}
.palette { list-style: none; padding: 0; margin: 8px 0; display: grid; gap: 8px; }
.pal-node { border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px; display: flex; justify-content: space-between; align-items: flex-start; }
.node-info { display: flex; flex-direction: column; gap: 2px; cursor: pointer; flex: 1; }
.sub { color: #6b7280; font-size: 12px; }
.ip-preview { color: #6b7280; font-size: 11px; }
.node-actions { display: flex; gap: 4px; margin-left: 8px; }
.edit-btn, .delete-btn { 
  background: none; 
  border: none; 
  padding: 4px; 
  cursor: pointer; 
  border-radius: 4px; 
  font-size: 14px;
  transition: all 0.2s ease;
}
.edit-btn:hover { 
  background: #dbeafe; 
  transform: translateY(-1px);
  box-shadow: 0 1px 3px rgba(59, 130, 246, 0.2);
}
.delete-btn:hover { 
  background: #fee2e2; 
  transform: translateY(-1px);
  box-shadow: 0 1px 3px rgba(239, 68, 68, 0.2);
}

/* 攻击链按钮样式 */
.attack-chain-section { margin-top: 16px; padding-top: 12px; border-top: 1px solid #e5e7eb; }
/* 底部栏按钮样式已统一 */
.server-info { 
  margin-top: 8px; 
  text-align: center; 
  color: #6b7280; 
  font-size: 11px; 
  line-height: 1.3;
}

/* 状态管理按钮样式 */
.state-management-section { margin-top: 12px; padding-top: 12px; border-top: 1px solid #e5e7eb; }
.state-buttons { display: flex; gap: 6px; margin-bottom: 8px; }
/* 底部栏按钮样式已统一 */
.auto-save-info { 
  text-align: center; 
  color: #6b7280; 
  font-size: 10px; 
  line-height: 1.3;
}

/* 攻击目标样式 */
.attack-target { 
  position: absolute; 
  z-index: 4; 
  display: flex; 
  flex-direction: column; 
  align-items: center;
  user-select: none;
  transition: filter 0.2s;
  width: auto; /* 改为自动宽度，允许IP输入框扩展 */
  min-width: 60px; /* 最小宽度确保连接点计算正确 */
  height: auto;
  min-height: 60px;
  overflow: visible; /* 允许子元素超出容器 */
}

.attack-target:active {
  filter: brightness(1.1);
}

.target-icon { 
  filter: drop-shadow(0 4px 8px rgba(220, 38, 38, 0.3)); 
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 
}

.attack-target:hover .target-icon { 
  transform: scale(1.15) rotate(5deg); 
  filter: drop-shadow(0 6px 12px rgba(220, 38, 38, 0.4));
}
.target-ip-input { 
  width: auto;
  min-width: 56px;
  max-width: 120px;
  margin-top: 4px; 
  box-sizing: border-box; 
  padding: 2px 6px; 
  border: 1px solid #dc2626; 
  border-radius: 4px; 
  font-size: 10px; 
  text-align: center; 
  background: rgba(255, 255, 255, 0.9);
  color: #dc2626;
  font-weight: 600;
}

/* 受攻击类型切换按钮 */
.victim-type-switch {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 4px;
  padding: 4px 8px;
  background: rgba(255, 255, 255, 0.95);
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  z-index: 10;
  position: relative;
  min-width: 140px;
  width: auto;
  margin-left: -40px;
  margin-right: -40px;
  cursor: pointer;
}

.switch-label {
  font-size: 10px;
  color: #374151;
  font-weight: 500;
  padding: 0 2px;
  min-width: fit-content;
  white-space: nowrap;
}

.switch-container {
  position: relative;
  width: 36px;
  height: 18px;
  background: #d1d5db;
  border-radius: 9px;
  cursor: pointer;
  transition: background 0.3s;
  flex-shrink: 0;
}

.switch-container:hover {
  background: #9ca3af;
}

.victim-type-switch:has(.switch-slider-right) .switch-container {
  background: #3b82f6;
}

.victim-type-switch:has(.switch-slider-right) .switch-container:hover {
  background: #2563eb;
}

.switch-slider {
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  background: #fff;
  border-radius: 50%;
  transition: transform 0.3s;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}

.switch-slider-right {
  transform: translateX(18px);
}
.target-ip-input:focus { 
  outline: none; 
  border-color: #991b1b; 
  background: #fff;
  box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.2);
}
.target-ip-input::placeholder { color: #fca5a5; }

/* 右下角拖拽手柄 */

/* 弹出窗口样式 */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 2000; }
.modal { background: #fff; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); width: 90%; max-width: 520px; max-height: 90vh; overflow-y: auto; position: relative; }
.modal-header { 
  display: flex; 
  justify-content: space-between; 
  align-items: center; 
  padding: 16px 20px; 
  padding-right: 50px; /* 为固定关闭按钮留出空间 */ 
  border-bottom: 1px solid #e5e7eb; 
  position: sticky;
  top: 0;
  background: #fff;
  z-index: 10;
  border-radius: 8px 8px 0 0;
}
.modal-header h3 { margin: 0; font-size: 18px; font-weight: 600; }
.close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #6b7280; }
.close-btn:hover { color: #374151; }
/* 固定在弹窗右上角的关闭按钮 */
.fixed-close-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 100;
  width: 32px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(4px);
  border-radius: 50%;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: all 0.2s ease;
  font-size: 20px;
  line-height: 1;
}
.fixed-close-btn:hover {
  background: rgba(255, 255, 255, 1);
  color: #dc2626;
  transform: scale(1.1);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.modal-body { padding: 20px; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 4px; font-weight: 500; color: #374151; }
.form-group input, .form-group select { width: 100%; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; box-sizing: border-box; }
.form-group input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); }
.form-group input[readonly] { background-color: #f9fafb; color: #6b7280; }
.backend-ip-input { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.backend-ip-input label { margin-bottom: 0; min-width: 80px; flex-shrink: 0; }
.backend-ip-input input { flex: 1; padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; box-sizing: border-box; }
.modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 16px 20px; border-top: 1px solid #e5e7eb; }
.btn-cancel { padding: 8px 16px; border: 1px solid #d1d5db; background: #fff; color: #374151; border-radius: 6px; cursor: pointer; }
.btn-cancel:hover { background: #f9fafb; }
.btn-confirm {
  padding: 8px 16px;
  border: none;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #fff;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.2s ease;
  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}

.btn-confirm:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: 0 4px 16px rgba(102, 126, 234, 0.4);
}

.btn-confirm:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}

.btn-confirm:disabled {
  background: #9ca3af;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

/* 服务器节点参数区域 */
.server-params-section {
  margin-top: 20px;
  padding-top: 20px;
  border-top: 2px solid #e5e7eb;
}

.section-divider {
  text-align: center;
  margin-bottom: 16px;
}

.section-divider span {
  background: #f3f4f6;
  padding: 4px 12px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 600;
  color: #6b7280;
}

.backend-configs {
  margin-top: 12px;
}

.backend-configs > label {
  display: block;
  margin-bottom: 8px;
  font-weight: 500;
  color: #374151;
}

.backend-config-item {
  margin-bottom: 12px;
  padding: 12px;
  background: #f9fafb;
  border-radius: 6px;
  border-left: 3px solid #10b981;
}

.backend-config-header {
  font-weight: 600;
  color: #1f2937;
  margin-bottom: 8px;
  font-size: 13px;
}

.backend-config-form {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.form-group.compact {
  margin-bottom: 0;
}

.form-group.compact label {
  font-size: 12px;
  color: #6b7280;
}

.form-group.compact input {
  padding: 6px 10px;
  font-size: 13px;
}

/* 可展开区域样式 */
.expandable-section {
  margin-bottom: 16px;
  border: 1px solid #e5e7eb;
  border-radius: 6px;
  overflow: hidden;
}

.expand-toggle-btn {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px 16px;
  background: #f9fafb;
  border: none;
  border-bottom: 1px solid #e5e7eb;
  cursor: pointer;
  font-weight: 500;
  color: #374151;
  transition: all 0.2s;
  text-align: left;
}

.expand-toggle-btn:hover {
  background: #f3f4f6;
  color: #1f2937;
}

.expand-icon {
  font-size: 12px;
  color: #6b7280;
  transition: transform 0.2s;
  display: inline-block;
  width: 16px;
}

.expandable-content {
  padding: 16px;
  background: #fff;
}

.expandable-content .form-group {
  margin-bottom: 12px;
}

.expandable-content .form-group:last-child {
  margin-bottom: 0;
}

/* 任务选择窗口样式 */
.task-modal {
  max-width: 600px;
}

.config-modal {
  max-width: 520px;
}

.config-section {
  margin-bottom: 20px;
}

.config-row {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  align-items: end;
  margin-bottom: 12px;
}

.config-field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.config-field.single {
  grid-column: span 2;
}

.config-field label {
  font-size: 13px;
  font-weight: 500;
  color: #374151;
}

.config-field input {
  border: 1px solid #d1d5db;
  border-radius: 8px;
  padding: 10px 12px;
  font-size: 14px;
  transition: all 0.2s ease;
  box-sizing: border-box;
}

.config-field input:focus {
  outline: none;
  border-color: #667eea;
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.12);
}

.config-apply-btn {
  align-self: stretch;
  padding: 10px 18px;
  border: none;
  border-radius: 8px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s ease;
  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}

.config-apply-btn.secondary {
  background: linear-gradient(135deg, #5b67d6 0%, #7050b8 100%);
  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}

.config-apply-btn:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.35);
}

.config-apply-btn:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}

.config-apply-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.config-apply-btn:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 4px 12px rgba(34, 197, 94, 0.35);
}

.config-apply-btn:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3);
}

/* 国家选择样式 */
.config-section-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}

.config-section-header label {
  font-weight: 600;
  color: #374151;
  font-size: 14px;
}

.selected-count {
  font-size: 12px;
  color: #667eea;
  font-weight: 500;
}

/* 攻击链优化样式 */
.toggle-switch {
  position: relative;
  display: inline-block;
  width: 44px;
  height: 24px;
  margin-left: 12px;
}

.toggle-switch input {
  opacity: 0;
  width: 0;
  height: 0;
}

.toggle-slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  transition: 0.3s;
  border-radius: 24px;
}

.toggle-slider:before {
  position: absolute;
  content: "";
  height: 18px;
  width: 18px;
  left: 3px;
  bottom: 3px;
  background-color: white;
  transition: 0.3s;
  border-radius: 50%;
}

.toggle-switch input:checked + .toggle-slider {
  background-color: #667eea;
}

.toggle-switch input:checked + .toggle-slider:before {
  transform: translateX(20px);
}

.optimize-item-wrapper {
  margin-top: 12px;
  background: #fff;
  border-radius: 6px;
  overflow: visible;
  border: 1px solid #e5e7eb;
}

.optimize-header {
  display: flex;
  align-items: center;
  padding: 10px 12px;
  cursor: pointer;
  user-select: none;
  background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
  transition: all 0.2s ease;
}

.optimize-header:hover {
  background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
}

.optimize-header-text {
  flex: 1;
  font-weight: 600;
  color: #374151;
  font-size: 13px;
}

.optimize-items {
  padding: 12px;
  background: #f9fafb;
  overflow: visible;
  position: relative;
}

.optimize-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 0;
  border-bottom: 1px solid #e5e7eb;
  position: relative;
  overflow: visible;
}

.optimize-item:last-child {
  border-bottom: none;
}

.optimize-name {
  font-size: 13px;
  color: #374151;
  font-weight: 500;
}

.info-icon-wrapper {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 8px;
  z-index: 10;
}

.info-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #667eea;
  color: white;
  font-size: 12px;
  font-weight: 600;
  font-style: normal;
  cursor: help;
  transition: all 0.2s;
}

.info-icon:hover {
  background: #5568d3;
  transform: scale(1.1);
}

.info-tooltip {
  position: absolute;
  bottom: 100%;
  right: 0;
  margin-bottom: 8px;
  padding: 8px 12px;
  background: #1f2937;
  color: white;
  font-size: 12px;
  line-height: 1.5;
  border-radius: 6px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
  z-index: 10000;
  min-width: 200px;
  max-width: 350px;
  white-space: normal;
  word-wrap: break-word;
  word-break: normal;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

.info-tooltip::after {
  content: '';
  position: absolute;
  top: 100%;
  right: 12px;
  border: 6px solid transparent;
  border-top-color: #1f2937;
}

.info-icon-wrapper:hover .info-tooltip {
  opacity: 1;
}

.country-selector {
  max-height: 300px;
  overflow-y: auto;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 8px;
  background: #f9fafb;
}

.loading-countries,
.no-countries {
  text-align: center;
  padding: 20px;
  color: #6b7280;
  font-size: 13px;
}

.continent-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.continent-item {
  background: #fff;
  border-radius: 6px;
  overflow: hidden;
  border: 1px solid #e5e7eb;
}

.continent-header {
  display: flex;
  align-items: center;
  padding: 10px 12px;
  cursor: pointer;
  user-select: none;
  background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
  transition: all 0.2s ease;
}

.continent-header:hover {
  background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
}

.expand-icon {
  display: inline-block;
  margin-right: 8px;
  transition: transform 0.2s ease;
  color: #6b7280;
  font-size: 10px;
}

.expand-icon.expanded {
  transform: rotate(90deg);
}

.continent-name {
  flex: 1;
  font-weight: 600;
  color: #374151;
  font-size: 13px;
}

.continent-count {
  margin-right: 12px;
  font-size: 12px;
  color: #6b7280;
}

.continent-checkbox {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  font-size: 12px;
  color: #667eea;
  font-weight: 500;
  user-select: none;
}

.continent-checkbox input[type="checkbox"] {
  cursor: pointer;
}

.country-list {
  padding: 8px 12px 12px 40px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  background: #fff;
}

.country-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s ease;
  user-select: none;
}

.country-item:hover {
  background: #f3f4f6;
}

.country-item input[type="checkbox"] {
  cursor: pointer;
  flex-shrink: 0;
}

.country-name {
  flex: 1;
  font-size: 13px;
  color: #374151;
}

.country-name-en {
  font-size: 11px;
  color: #9ca3af;
  font-style: italic;
}

.config-apply-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.config-hint {
  font-size: 12px;
  color: #6b7280;
  margin: 0 0 10px 0;
}

.config-message {
  padding: 10px 12px;
  border-radius: 8px;
  font-size: 13px;
  line-height: 1.5;
}

.config-message.success {
  background: #ecfdf5;
  color: #047857;
  border: 1px solid rgba(16, 185, 129, 0.4);
}

.config-message.error {
  background: #fef2f2;
  color: #dc2626;
  border: 1px solid rgba(248, 113, 113, 0.4);
}

.config-message.idle,
.config-message.saving {
  background: #eef2ff;
  color: #4338ca;
  border: 1px solid rgba(99, 102, 241, 0.4);
}

.config-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid #e5e7eb;
}

.config-btn {
  flex: 1;
  min-width: 0;
  padding: 10px 20px;
  border: 1px solid #d1d5db;
  background: white;
  color: #374151;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.2s ease;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  text-align: center;
  width: 100%;
  box-sizing: border-box;
}

.config-btn:hover:not(:disabled) {
  background: #f9fafb;
  border-color: #9ca3af;
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}

.config-btn:active:not(:disabled) {
  transform: translateY(0);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}

.config-btn:disabled {
  background: #f3f4f6;
  color: #9ca3af;
  border-color: #e5e7eb;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}

.loading-indicator, .empty-tasks {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 40px;
  color: #6b7280;
  font-size: 14px;
}

.task-list-container {
  max-height: 60vh;
  overflow-y: auto;
}

.task-list-header {
  margin-bottom: 12px;
  font-weight: 600;
  color: #374151;
  font-size: 14px;
}

.task-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.task-item {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 12px;
  transition: all 0.2s;
}

.task-item:hover {
  border-color: #667eea;
  background: #f9fafb;
}

.task-checkbox {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  cursor: pointer;
  width: 100%;
}

.task-checkbox input[type="checkbox"] {
  margin-top: 4px;
  width: 18px;
  height: 18px;
  cursor: pointer;
}

.task-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.task-id {
  font-weight: 600;
  color: #1f2937;
  font-size: 14px;
}

.task-details {
  display: flex;
  gap: 12px;
  align-items: center;
  font-size: 12px;
}

.task-time {
  color: #6b7280;
}

.task-status {
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 11px;
  font-weight: 500;
}

.task-status.completed {
  background: #d1fae5;
  color: #065f46;
}

.task-status.running {
  background: #dbeafe;
  color: #1e40af;
}

.task-status.failed {
  background: #fee2e2;
  color: #991b1b;
}

.task-node-count {
  color: #667eea;
  font-weight: 500;
  padding: 2px 8px;
  background: #eef2ff;
  border-radius: 4px;
  white-space: nowrap;
}

/* 发送状态样式 */
.send-result {
  margin-top: 8px;
  padding: 6px 8px;
  border-radius: 4px;
  text-align: center;
  font-size: 11px;
}

.send-result.success {
  background: #d1fae5;
  color: #065f46;
  border: 1px solid #a7f3d0;
}

.send-result.error {
  background: #fee2e2;
  color: #991b1b;
  border: 1px solid #fca5a5;
}

.send-result.idle {
  background: #f3f4f6;
  color: #374151;
  border: 1px solid #d1d5db;
}


.test-connection-btn:disabled {
  background: #9ca3af;
  cursor: not-allowed;
  transform: none;
}

.test-connection-btn:disabled:hover {
  background: #9ca3af;
  transform: none;
  box-shadow: none;
}

/* 底部栏按钮样式已统一 */

/* CSV管理按钮样式 */
.csv-management-section { 
  margin-top: 12px; 
  padding-top: 12px; 
  border-top: 1px solid #e5e7eb; 
}
.csv-buttons { 
  display: flex; 
  gap: 6px; 
  margin-bottom: 8px; 
}
/* 底部栏按钮样式已统一 */
.csv-result {
  margin-top: 8px;
  padding: 6px 8px;
  border-radius: 4px;
  text-align: center;
  font-size: 11px;
}
.csv-result.success {
  background: #d1fae5;
  color: #065f46;
  border: 1px solid #a7f3d0;
}
.csv-result.error {
  background: #fee2e2;
  color: #991b1b;
  border: 1px solid #fca5a5;
}
.csv-result.info {
  background: #dbeafe;
  color: #1e40af;
  border: 1px solid #93c5fd;
}

/* 新增节点按钮样式 */

.node-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow-y: auto;
  flex: 1; /* 自动填充剩余空间 */
  min-height: 0; /* 允许 flex 子元素缩小 */
  position: relative;
  /* 优化滚动性能 */
  will-change: scroll-position;
  -webkit-overflow-scrolling: touch;
}

.node-item {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 6px;
  padding: 8px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
  cursor: grab;
  min-height: 80px; /* 固定最小高度，用于虚拟滚动计算 */
  box-sizing: border-box;
  /* 优化渲染性能 */
  contain: layout style paint;
}

.node-index {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: #ffffff;
  font-weight: bold;
  font-size: 11px;
  border-radius: 5px;
  flex-shrink: 0;
  box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
}

.node-item:hover {
  background: linear-gradient(135deg, #ffffff 0%, #f0f4ff 100%);
  border-color: #667eea40;
  box-shadow: 
    0 4px 12px rgba(102, 126, 234, 0.15),
    0 2px 4px rgba(0, 0, 0, 0.08);
  transform: translateX(-2px);
}

.node-item:active {
  cursor: grabbing;
}

.node-item.deployed {
  opacity: 0.6;
  cursor: not-allowed;
  background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
}

.node-item.deployed:hover {
  transform: none;
  border-color: #e5e7eb;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}


.node-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
  overflow: hidden;
}

.node-info {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 3px;
  min-width: 0;
}

.ip-info-small, .backend-ips-small {
  font-size: 11px;
  color: #6b7280;
  padding: 2px 0;
}

.param-info {
  font-size: 10px;
  color: #9ca3af;
}

.backend-section {
  margin-top: 6px;
  padding-top: 6px;
  border-top: 1px solid #e5e7eb;
}

.section-title {
  font-size: 11px;
  font-weight: 600;
  color: #6b7280;
  margin-bottom: 4px;
}

.backend-detail {
  margin-bottom: 6px;
  padding: 4px 6px;
  background: #f9fafb;
  border-radius: 4px;
  border-left: 2px solid #10b981;
}

.backend-ip {
  font-size: 11px;
  color: #374151;
  font-weight: 500;
  margin-bottom: 3px;
}

.backend-params {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.param-item {
  font-size: 10px;
  color: #6b7280;
  background: #ffffff;
  padding: 2px 6px;
  border-radius: 3px;
  border: 1px solid #e5e7eb;
  white-space: nowrap;
}

.node-header {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  min-width: 0;
}

.node-header strong {
  font-size: 13px;
  color: #111827;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
  min-width: 0;
}

.deployed-badge {
  background: linear-gradient(135deg, #10b981, #059669);
  color: white;
  padding: 1px 6px;
  border-radius: 10px;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 0.3px;
  box-shadow: 0 1px 3px rgba(16, 185, 129, 0.3);
  flex-shrink: 0;
}

.node-details {
  display: flex;
  gap: 6px;
  font-size: 11px;
  color: #6b7280;
  flex-wrap: wrap;
}

.backend-count {
  background: #f3f4f6;
  padding: 1px 5px;
  border-radius: 3px;
  font-size: 10px;
  white-space: nowrap;
}

.ip-info-small, .backend-ips-small {
  font-size: 10px;
  color: #6b7280;
  font-family: monospace;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.node-actions {
  display: flex;
  gap: 3px;
  align-items: center;
  justify-content: flex-end;
  flex-shrink: 0;
  flex-wrap: wrap;
}

/* 部署按钮已删除，改为拖拽部署 */

.edit-btn, .delete-btn {
  background: none;
  border: none;
  padding: 3px;
  cursor: pointer;
  border-radius: 3px;
  transition: all 0.2s ease;
  font-size: 13px;
  line-height: 1;
}

.edit-btn:hover {
  background: #dbeafe;
  transform: translateY(-1px);
  box-shadow: 0 1px 3px rgba(59, 130, 246, 0.2);
}

.delete-btn:hover {
  background: #fee2e2;
  transform: translateY(-1px);
  box-shadow: 0 1px 3px rgba(239, 68, 68, 0.2);
}

/* 攻击配置对话框样式 */
.dialog-mask {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  backdrop-filter: blur(4px);
}

.attack-config-dialog {
  width: 600px;
  max-width: calc(100vw - 64px);
  background: white;
  border-radius: 16px;
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
  overflow: hidden;
  max-height: 80vh;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
}

.attack-config-dialog .dialog-header {
  padding: 24px 32px 16px;
  background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
  border-bottom: 1px solid #e2e8f0;
  box-sizing: border-box;
}

.attack-config-dialog .dialog-header h3 {
  margin: 0 0 4px 0;
  font-size: 20px;
  font-weight: 600;
  color: #1e293b;
}

.attack-config-dialog .dialog-subtitle {
  font-size: 14px;
  color: #64748b;
}

.attack-config-dialog .dialog-content {
  padding: 24px 32px;
  flex: 1;
  overflow-y: auto;
  overflow-x: hidden;
  box-sizing: border-box;
}

.attack-config-dialog .form-row {
  margin-bottom: 20px;
  box-sizing: border-box;
  width: 100%;
  overflow-x: hidden;
}

.attack-config-dialog .form-row:last-child {
  margin-bottom: 0;
}

.attack-config-dialog .form-row label {
  display: block;
  font-size: 14px;
  font-weight: 500;
  color: #374151;
  margin-bottom: 8px;
}

.attack-config-dialog input,
.attack-config-dialog textarea {
  width: 100%;
  box-sizing: border-box;
  border: 1px solid #d1d5db;
  border-radius: 8px;
  padding: 12px 16px;
  font-size: 14px;
  transition: all 0.2s ease;
  background: white;
  font-family: 'Courier New', monospace;
  resize: vertical;
}

.attack-config-dialog input:focus,
.attack-config-dialog textarea:focus {
  outline: none;
  border-color: #667eea;
  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}

.attack-config-dialog textarea {
  min-height: 100px;
  line-height: 1.5;
}

.attack-config-dialog .input-hint {
  margin-top: 8px;
  font-size: 12px;
  color: #6b7280;
  line-height: 1.6;
}

.attack-config-dialog .dialog-actions {
  display: flex;
  gap: 12px;
  justify-content: flex-end;
  padding: 20px 32px;
  background: #f8fafc;
  border-top: 1px solid #e2e8f0;
  box-sizing: border-box;
}

.attack-config-dialog .btn-cancel {
  padding: 10px 20px;
  border: 1px solid #d1d5db;
  background: white;
  color: #374151;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.2s ease;
}

.attack-config-dialog .btn-cancel:hover {
  background: #f9fafb;
  border-color: #9ca3af;
}

.attack-config-dialog .btn-confirm {
  padding: 10px 20px;
  border: none;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 8px;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  transition: all 0.2s ease;
  box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
}

.attack-config-dialog .btn-confirm:hover {
  transform: translateY(-1px);
  box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
}

.attack-config-dialog .btn-confirm:active {
  transform: translateY(0);
  box-shadow: 0 2px 4px rgba(102, 126, 234, 0.2);
}
</style>


